diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-05-01 04:05:16 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-05-01 04:05:16 +0200 |
commit | 4c03a1200eb947a0ed13f78b46fd670601b8cb80 (patch) | |
tree | 16c64421a72000ab19f939ffe492519b013fbafc /src | |
parent | bb6d8317a5ff672fccdb0a35e55077521827a48d (diff) |
implement payback (with rudimentary UI)
Diffstat (limited to 'src')
-rw-r--r-- | src/cryptoApi-test.ts | 35 | ||||
-rw-r--r-- | src/cryptoApi.ts | 27 | ||||
-rw-r--r-- | src/cryptoWorker.ts | 37 | ||||
-rw-r--r-- | src/emscriptif.ts | 27 | ||||
-rw-r--r-- | src/pages/payback.html | 37 | ||||
-rw-r--r-- | src/pages/payback.tsx | 99 | ||||
-rw-r--r-- | src/pages/popup.tsx | 7 | ||||
-rw-r--r-- | src/types.ts | 94 | ||||
-rw-r--r-- | src/wallet.ts | 137 | ||||
-rw-r--r-- | src/wxApi.ts | 8 | ||||
-rw-r--r-- | src/wxBackend.ts | 11 |
11 files changed, 460 insertions, 59 deletions
diff --git a/src/cryptoApi-test.ts b/src/cryptoApi-test.ts index dde3ea899..8350defbc 100644 --- a/src/cryptoApi-test.ts +++ b/src/cryptoApi-test.ts @@ -1,42 +1,46 @@ import {CryptoApi} from "./cryptoApi"; -import {ReserveRecord, DenominationRecord, denominationRecordFromKeys} from "./types"; +import {ReserveRecord, DenominationRecord, DenominationStatus} from "./types"; import {test, TestLib} from "talertest"; let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; -let denomValid1: DenominationRecord = denominationRecordFromKeys("https://example.com/exchange", { - "master_sig": "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", - "stamp_start": "/Date(1473148381)/", - "stamp_expire_withdraw": "/Date(2482300381)/", - "stamp_expire_deposit": "/Date(1851580381)/", - "denom_pub": "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", - "stamp_expire_legal": "/Date(1567756381)/", - "value": { +let denomValid1: DenominationRecord = { + masterSig: "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", + stampStart: "/Date(1473148381)/", + stampExpireWithdraw: "/Date(2482300381)/", + stampExpireDeposit: "/Date(1851580381)/", + denomPub: "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", + stampExpireLegal: "/Date(1567756381)/", + value: { "currency": "PUDOS", "value": 0, "fraction": 100000 }, - "fee_withdraw": { + feeWithdraw: { "currency": "PUDOS", "value": 0, "fraction": 10000 }, - "fee_deposit": { + feeDeposit: { "currency": "PUDOS", "value": 0, "fraction": 10000 }, - "fee_refresh": { + feeRefresh: { "currency": "PUDOS", "value": 0, "fraction": 10000 }, - "fee_refund": { + feeRefund: { "currency": "PUDOS", "value": 0, "fraction": 10000 - } -}); + }, + denomPubHash: "dummy", + status: DenominationStatus.Unverified, + isOffered: true, + exchangeBaseUrl: "https://exchange.example.com/", +}; let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1)); denomInvalid1.value.value += 1; @@ -55,6 +59,7 @@ test("precoin creation", async (t: TestLib) => { let r: ReserveRecord = { reserve_pub: pub, reserve_priv: priv, + hasPayback: false, exchange_base_url: "https://example.com/exchange", created: 0, requested_amount: {currency: "PUDOS", value: 0, fraction: 0}, diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts index 5657d74d6..8dd14392a 100644 --- a/src/cryptoApi.ts +++ b/src/cryptoApi.ts @@ -22,13 +22,20 @@ import { - PreCoinRecord, CoinRecord, ReserveRecord, AmountJson, - DenominationRecord + PreCoinRecord, + CoinRecord, + ReserveRecord, + AmountJson, + DenominationRecord, + PaybackRequest, + RefreshSessionRecord, + WireFee, + PayCoinInfo, } from "./types"; -import {OfferRecord} from "./wallet"; -import {CoinWithDenom} from "./wallet"; -import {PayCoinInfo} from "./types"; -import {RefreshSessionRecord, WireFee} from "./types"; +import { + OfferRecord, + CoinWithDenom, +} from "./wallet"; interface WorkerState { @@ -230,6 +237,10 @@ export class CryptoApi { return this.doRpc<string>("hashString", 1, str); } + hashDenomPub(denomPub: string): Promise<string> { + return this.doRpc<string>("hashDenomPub", 1, denomPub); + } + isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> { return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub); @@ -256,6 +267,10 @@ export class CryptoApi { return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk); } + createPaybackRequest(coin: CoinRecord, preCoin: PreCoinRecord): Promise<PaybackRequest> { + return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin, preCoin); + } + createRefreshSession(exchangeBaseUrl: string, kappa: number, meltCoin: CoinRecord, diff --git a/src/cryptoWorker.ts b/src/cryptoWorker.ts index 55c08d4b5..a11a0d021 100644 --- a/src/cryptoWorker.ts +++ b/src/cryptoWorker.ts @@ -23,8 +23,14 @@ import * as native from "./emscriptif"; import { - PreCoinRecord, PayCoinInfo, AmountJson, - RefreshSessionRecord, RefreshPreCoinRecord, ReserveRecord, CoinStatus, + PreCoinRecord, + PayCoinInfo, + AmountJson, + RefreshSessionRecord, + RefreshPreCoinRecord, + ReserveRecord, + CoinStatus, + PaybackRequest, } from "./types"; import create = chrome.alarms.create; import {OfferRecord} from "./wallet"; @@ -96,8 +102,29 @@ namespace RpcFunctions { return preCoin; } + export function createPaybackRequest(coin: CoinRecord, preCoin: PreCoinRecord): PaybackRequest { + if (coin.coinPub != preCoin.coinPub) { + throw Error("coin doesn't match precoin"); + } + let p = new native.PaybackRequestPS({ + coin_pub: native.EddsaPublicKey.fromCrock(coin.coinPub), + h_denom_pub: native.RsaPublicKey.fromCrock(coin.denomPub).encode().hash(), + coin_blind: native.RsaBlindingKeySecret.fromCrock(preCoin.blindingKey), + }); + let coinPriv = native.EddsaPrivateKey.fromCrock(coin.coinPriv); + let coinSig = native.eddsaSign(p.toPurpose(), coinPriv); + let paybackRequest: PaybackRequest = { + denom_pub: coin.denomPub, + denom_sig: coin.denomSig, + coin_blind_key_secret: preCoin.blindingKey, + coin_pub: coin.coinPub, + coin_sig: coinSig.toCrock(), + }; + return paybackRequest; + } - export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string) { + + export function isValidPaymentSignature(sig: string, contractHash: string, merchantPub: string): boolean { let p = new native.PaymentSignaturePS({ contract_hash: native.HashCode.fromCrock(contractHash), }); @@ -366,6 +393,10 @@ namespace RpcFunctions { const b = native.ByteArray.fromStringWithNull(str); return b.hash().toCrock(); } + + export function hashDenomPub(denomPub: string): string { + return native.RsaPublicKey.fromCrock(denomPub).encode().hash().toCrock(); + } } diff --git a/src/emscriptif.ts b/src/emscriptif.ts index 347ee54a0..caa0fb8cc 100644 --- a/src/emscriptif.ts +++ b/src/emscriptif.ts @@ -208,6 +208,7 @@ export enum SignaturePurpose { TEST = 4242, MERCHANT_PAYMENT_OK = 1104, MASTER_WIRE_FEES = 1028, + WALLET_COIN_PAYBACK = 1203, } @@ -966,6 +967,32 @@ export class WithdrawRequestPS extends SignatureStruct { } +export interface PaybackRequestPS_args { + coin_pub: EddsaPublicKey; + h_denom_pub: HashCode; + coin_blind: RsaBlindingKeySecret; +} + + +export class PaybackRequestPS extends SignatureStruct { + constructor(w: PaybackRequestPS_args) { + super(w); + } + + purpose() { + return SignaturePurpose.WALLET_COIN_PAYBACK; + } + + fieldTypes() { + return [ + ["coin_pub", EddsaPublicKey], + ["h_denom_pub", HashCode], + ["coin_blind", RsaBlindingKeySecret], + ]; + } +} + + interface RefreshMeltCoinAffirmationPS_Args { session_hash: HashCode; amount_with_fee: AmountNbo; diff --git a/src/pages/payback.html b/src/pages/payback.html new file mode 100644 index 000000000..d7b913eec --- /dev/null +++ b/src/pages/payback.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>Taler Wallet: Payback</title> + + <link rel="stylesheet" type="text/css" href="../style/lang.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <link rel="icon" href="/img/icon.png"> + + <script src="/dist/page-common-bundle.js"></script> + <script src="/dist/payback-bundle.js"></script> + + <style> + body { + font-size: 100%; + } + .tree-item { + margin: 2em; + border-radius: 5px; + border: 1px solid gray; + padding: 1em; + } + .button-linky { + background: none; + color: black; + text-decoration: underline; + border: none; + } + </style> + + <body> + <div id="container"></div> + </body> +</html> diff --git a/src/pages/payback.tsx b/src/pages/payback.tsx new file mode 100644 index 000000000..9e463d4a0 --- /dev/null +++ b/src/pages/payback.tsx @@ -0,0 +1,99 @@ +/* + This file is part of TALER + (C) 2017 Inria + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * View and edit auditors. + * + * @author Florian Dold + */ + + +import { + ExchangeRecord, + ExchangeForCurrencyRecord, + DenominationRecord, + AuditorRecord, + CurrencyRecord, + ReserveRecord, + CoinRecord, + PreCoinRecord, + Denomination, + WalletBalance, +} from "../types"; +import { ImplicitStateComponent, StateHolder } from "../components"; +import { + getCurrencies, + updateCurrency, + getPaybackReserves, + withdrawPaybackReserve, +} from "../wxApi"; +import { prettyAmount } from "../renderHtml"; +import { getTalerStampDate } from "../helpers"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +class Payback extends ImplicitStateComponent<any> { + reserves: StateHolder<ReserveRecord[]|null> = this.makeState(null); + constructor() { + super(); + let port = chrome.runtime.connect(); + port.onMessage.addListener((msg: any) => { + if (msg.notify) { + console.log("got notified"); + this.update(); + } + }); + this.update(); + } + + async update() { + let reserves = await getPaybackReserves(); + this.reserves(reserves); + } + + withdrawPayback(pub: string) { + withdrawPaybackReserve(pub); + } + + render(): JSX.Element { + let reserves = this.reserves(); + if (!reserves) { + return <span>loading ...</span>; + } + if (reserves.length == 0) { + return <span>No reserves with payback available.</span>; + } + return ( + <div> + {reserves.map(r => ( + <div> + <h2>Reserve for ${prettyAmount(r.current_amount!)}</h2> + <ul> + <li>Exchange: ${r.exchange_base_url}</li> + </ul> + <button onClick={() => this.withdrawPayback(r.reserve_pub)}>Withdraw again</button> + </div> + ))} + </div> + ); + } +} + +export function main() { + ReactDOM.render(<Payback />, document.getElementById("container")!); +} + +document.addEventListener("DOMContentLoaded", main); diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx index fc6d39a0a..9b375097f 100644 --- a/src/pages/popup.tsx +++ b/src/pages/popup.tsx @@ -299,8 +299,12 @@ class WalletBalanceView extends React.Component<any, any> { return <span></span>; } console.log(wallet); + let paybackAvailable = false; let listing = Object.keys(wallet).map((key) => { let entry: WalletBalanceEntry = wallet[key]; + if (entry.paybackAmount.value != 0 || entry.paybackAmount.fraction != 0) { + paybackAvailable = true; + } return ( <p> {bigAmount(entry.available)} @@ -311,9 +315,12 @@ class WalletBalanceView extends React.Component<any, any> { }); let link = chrome.extension.getURL("/src/pages/auditors.html"); let linkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>; + let paybackLink = chrome.extension.getURL("/src/pages/payback.html"); + let paybackLinkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>; return ( <div> {listing.length > 0 ? listing : this.renderEmpty()} + {paybackAvailable && paybackLinkElem} {linkElem} </div> ); diff --git a/src/types.ts b/src/types.ts index e357dfa26..4964d9f45 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,7 +73,13 @@ export interface ReserveRecord { precoin_amount: AmountJson; - confirmed: boolean, + confirmed: boolean; + + /** + * We got some payback to this reserve. We'll cease to automatically + * withdraw money from it. + */ + hasPayback: boolean; } export interface AuditorRecord { @@ -127,6 +133,9 @@ export class DenominationRecord { @Checkable.String denomPub: string; + @Checkable.String + denomPubHash: string; + @Checkable.Value(AmountJson) feeWithdraw: AmountJson; @@ -276,27 +285,65 @@ export interface RefreshPreCoinRecord { publicKey: string; privateKey: string; coinEv: string; - blindingKey: string -} - -export function denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): DenominationRecord { - let d: DenominationRecord = { - denomPub: denomIn.denom_pub, - exchangeBaseUrl: exchangeBaseUrl, - feeDeposit: denomIn.fee_deposit, - masterSig: denomIn.master_sig, - feeRefund: denomIn.fee_refund, - feeRefresh: denomIn.fee_refresh, - feeWithdraw: denomIn.fee_withdraw, - stampExpireDeposit: denomIn.stamp_expire_deposit, - stampExpireLegal: denomIn.stamp_expire_legal, - stampExpireWithdraw: denomIn.stamp_expire_withdraw, - stampStart: denomIn.stamp_start, - status: DenominationStatus.Unverified, - isOffered: true, - value: denomIn.value, - }; - return d; + blindingKey: string; +} + +export interface PaybackRequest { + denom_pub: string; + + /** + * Signature over the coin public key by the denomination. + */ + denom_sig: string; + + coin_pub: string; + + coin_blind_key_secret: string; + + coin_sig: string; +} + +@Checkable.Class +export class PaybackConfirmation { + /** + * public key of the reserve that will receive the payback. + */ + @Checkable.String + reserve_pub: string; + + /** + * How much will the exchange pay back (needed by wallet in + * case coin was partially spent and wallet got restored from backup) + */ + @Checkable.Value(AmountJson) + amount: AmountJson; + + /** + * Time by which the exchange received the /payback request. + */ + @Checkable.String + timestamp: string; + + /** + * the EdDSA signature of TALER_PaybackConfirmationPS using a current + * signing key of the exchange affirming the successful + * payback request, and that the exchange promises to transfer the funds + * by the date specified (this allows the exchange delaying the transfer + * a bit to aggregate additional payback requests into a larger one). + */ + @Checkable.String + exchange_sig: string; + + /** + * Public EdDSA key of the exchange that was used to generate the signature. + * Should match one of the exchange's signing keys from /keys. It is given + * explicitly as the client might otherwise be confused by clock skew as to + * which signing key was used. + */ + @Checkable.String + exchange_pub: string; + + static checked: (obj: any) => PaybackConfirmation; } /** @@ -367,7 +414,7 @@ export interface CoinPaySig { export enum CoinStatus { - Fresh, TransactionPending, Dirty, Refreshed, + Fresh, TransactionPending, Dirty, Refreshed, PaybackPending, PaybackDone, } @@ -440,6 +487,7 @@ export interface WalletBalanceEntry { available: AmountJson; pendingIncoming: AmountJson; pendingPayment: AmountJson; + paybackAmount: AmountJson; } diff --git a/src/wallet.ts b/src/wallet.ts index 4c44b5d24..63cd597ea 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -44,8 +44,11 @@ import { WalletBalanceEntry, WireFee, ExchangeWireFeesRecord, - WireInfo, DenominationRecord, DenominationStatus, denominationRecordFromKeys, + WireInfo, + DenominationRecord, + DenominationStatus, CoinStatus, + PaybackConfirmation, } from "./types"; import { HttpRequestLibrary, @@ -410,6 +413,7 @@ export namespace Stores { } exchangeBaseUrlIndex = new Index<string,CoinRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); + denomPubIndex = new Index<string,CoinRecord>(this, "denomPub", "denomPub"); } class HistoryStore extends Store<HistoryRecord> { @@ -448,6 +452,7 @@ export namespace Stores { {keyPath: ["exchangeBaseUrl", "denomPub"] as any as IDBKeyPath}); } + denomPubHashIndex = new Index<string,DenominationRecord>(this, "denomPubHash", "denomPubHash"); exchangeBaseUrlIndex = new Index<string, DenominationRecord>(this, "exchangeBaseUrl", "exchangeBaseUrl"); denomPubIndex = new Index<string, DenominationRecord>(this, "denomPub", "denomPub"); } @@ -894,9 +899,8 @@ export class Wallet { try { let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url); - let reserve = await this.updateReserve(reserveRecord.reserve_pub, - exchange); - let n = await this.depleteReserve(reserve, exchange); + let reserve = await this.updateReserve(reserveRecord.reserve_pub); + let n = await this.depleteReserve(reserve); if (n != 0) { let depleted: HistoryRecord = { @@ -1013,6 +1017,7 @@ export class Wallet { const canonExchange = canonicalizeBaseUrl(req.exchange); const reserveRecord: ReserveRecord = { + hasPayback: false, reserve_pub: keypair.pub, reserve_priv: keypair.priv, exchange_base_url: canonExchange, @@ -1148,8 +1153,7 @@ export class Wallet { /** * Withdraw coins from a reserve until it is empty. */ - private async depleteReserve(reserve: ReserveRecord, - exchange: ExchangeRecord): Promise<number> { + private async depleteReserve(reserve: ReserveRecord): Promise<number> { console.log("depleting reserve"); if (!reserve.current_amount) { throw Error("can't withdraw when amount is unknown"); @@ -1158,7 +1162,7 @@ export class Wallet { if (!currentAmount) { throw Error("can't withdraw when amount is unknown"); } - let denomsForWithdraw = await this.getVerifiedWithdrawDenomList(exchange.baseUrl, + let denomsForWithdraw = await this.getVerifiedWithdrawDenomList(reserve.exchange_base_url, currentAmount); console.log(`withdrawing ${denomsForWithdraw.length} coins`); @@ -1204,14 +1208,13 @@ export class Wallet { * Update the information about a reserve that is stored in the wallet * by quering the reserve's exchange. */ - private async updateReserve(reservePub: string, - exchange: ExchangeRecord): Promise<ReserveRecord> { + private async updateReserve(reservePub: string): Promise<ReserveRecord> { let reserve = await this.q() .get<ReserveRecord>(Stores.reserves, reservePub); if (!reserve) { throw Error("reserve not in db"); } - let reqUrl = new URI("reserve/status").absoluteTo(exchange.baseUrl); + let reqUrl = new URI("reserve/status").absoluteTo(reserve.exchange_base_url); reqUrl.query({'reserve_pub': reservePub}); let resp = await this.http.get(reqUrl.href()); if (resp.status != 200) { @@ -1549,6 +1552,20 @@ export class Wallet { await this.q().put(Stores.exchangeWireFees, oldWireFees); + if (exchangeKeysJson.payback) { + for (let payback of exchangeKeysJson.payback) { + let denom = await this.q().getIndexed(Stores.denominations.denomPubHashIndex, payback.h_denom_pub); + if (!denom) { + continue; + } + console.log(`cashing back denom`, denom); + let coins = await this.q().iterIndex(Stores.coins.denomPubIndex, denom.denomPub).toArray(); + for (let coin of coins) { + this.payback(coin.coinPub); + } + } + } + return updatedExchangeInfo; } @@ -1571,7 +1588,7 @@ export class Wallet { const newAndUnseenDenoms: typeof existingDenoms = {}; for (let d of newKeys.denoms) { - let dr = denominationRecordFromKeys(exchangeInfo.baseUrl, d); + let dr = await this.denominationRecordFromKeys(exchangeInfo.baseUrl, d); if (!(d.denom_pub in existingDenoms)) { newAndUnseenDenoms[dr.denomPub] = dr; } @@ -1608,6 +1625,7 @@ export class Wallet { available: z, pendingIncoming: z, pendingPayment: z, + paybackAmount: z, }; } return entry; @@ -1643,6 +1661,17 @@ export class Wallet { return balance; } + function collectPaybacks(r: ReserveRecord, balance: WalletBalance) { + if (!r.hasPayback) { + return balance; + } + let entry = ensureEntry(balance, r.requested_amount.currency); + if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], r.current_amount!) < 0) { + entry.paybackAmount = Amounts.add(entry.paybackAmount, r.current_amount!).amount; + } + return balance; + } + function collectPendingRefresh(r: RefreshSessionRecord, balance: WalletBalance) { // Don't count finished refreshes, since the refresh already resulted @@ -1699,6 +1728,8 @@ export class Wallet { .reduce(collectPendingRefresh, balance); tx.iter(Stores.reserves) .reduce(collectPendingWithdraw, balance); + tx.iter(Stores.reserves) + .reduce(collectPaybacks, balance); tx.iter(Stores.transactions) .reduce(collectPayments, balance); await tx.finish(); @@ -2085,4 +2116,88 @@ export class Wallet { doPaymentSucceeded(); return; } + + async payback(coinPub: string): Promise<void> { + let coin = await this.q().get(Stores.coins, coinPub); + if (!coin) { + throw Error(`Coin ${coinPub} not found, can't request payback`); + } + let preCoin = await this.q().get(Stores.precoins, coin.coinPub); + if (!preCoin) { + throw Error(`Precoin of coin ${coinPub} not found`); + } + let reserve = await this.q().get(Stores.reserves, preCoin.reservePub); + if (!reserve) { + throw Error(`Reserve of coin ${coinPub} not found`); + } + switch (coin.status) { + case CoinStatus.Refreshed: + throw Error(`Can't do payback for coin ${coinPub} since it's refreshed`); + case CoinStatus.PaybackDone: + console.log(`Coin ${coinPub} already payed back`); + return; + } + coin.status = CoinStatus.PaybackPending; + // Even if we didn't get the payback yet, we suspend withdrawal, since + // technically we might update reserve status before we get the response + // from the reserve for the payback request. + reserve.hasPayback = true; + await this.q().put(Stores.coins, coin).put(Stores.reserves, reserve); + + let paybackRequest = await this.cryptoApi.createPaybackRequest(coin, preCoin); + let reqUrl = new URI("payback").absoluteTo(preCoin.exchangeBaseUrl); + let resp = await this.http.get(reqUrl.href()); + if (resp.status != 200) { + throw Error(); + } + let paybackConfirmation = PaybackConfirmation.checked(JSON.parse(resp.responseText)); + if (paybackConfirmation.reserve_pub != preCoin.reservePub) { + throw Error(`Coin's reserve doesn't match reserve on payback`); + } + coin = await this.q().get(Stores.coins, coinPub); + if (!coin) { + throw Error(`Coin ${coinPub} not found, can't confirm payback`); + } + coin.status = CoinStatus.PaybackDone; + await this.q().put(Stores.coins, coin); + await this.updateReserve(preCoin.reservePub); + } + + + async denominationRecordFromKeys(exchangeBaseUrl: string, denomIn: Denomination): Promise<DenominationRecord> { + let denomPubHash = await this.cryptoApi.hashDenomPub(denomIn.denom_pub); + let d: DenominationRecord = { + denomPubHash, + denomPub: denomIn.denom_pub, + exchangeBaseUrl: exchangeBaseUrl, + feeDeposit: denomIn.fee_deposit, + masterSig: denomIn.master_sig, + feeRefund: denomIn.fee_refund, + feeRefresh: denomIn.fee_refresh, + feeWithdraw: denomIn.fee_withdraw, + stampExpireDeposit: denomIn.stamp_expire_deposit, + stampExpireLegal: denomIn.stamp_expire_legal, + stampExpireWithdraw: denomIn.stamp_expire_withdraw, + stampStart: denomIn.stamp_start, + status: DenominationStatus.Unverified, + isOffered: true, + value: denomIn.value, + }; + return d; + } + + async withdrawPaybackReserve(reservePub: string): Promise<void> { + let reserve = await this.q().get(Stores.reserves, reservePub); + if (!reserve) { + throw Error(`Reserve ${reservePub} does not exist`); + } + reserve.hasPayback = false; + await this.q().put(Stores.reserves, reserve); + this.depleteReserve(reserve); + } + + async getPaybackReserves(): Promise<ReserveRecord[]> { + return await this.q().iter(Stores.reserves).filter(r => r.hasPayback).toArray() + } + } diff --git a/src/wxApi.ts b/src/wxApi.ts index bdc02af1b..0f460085e 100644 --- a/src/wxApi.ts +++ b/src/wxApi.ts @@ -84,6 +84,14 @@ export async function getReserves(exchangeBaseUrl: string): Promise<ReserveRecor return await callBackend("get-reserves", { exchangeBaseUrl }); } +export async function getPaybackReserves(): Promise<ReserveRecord[]> { + return await callBackend("get-payback-reserves"); +} + +export async function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> { + return await callBackend("withdraw-payback-reserve", { reservePub }); +} + export async function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> { return await callBackend("get-coins", { exchangeBaseUrl }); } diff --git a/src/wxBackend.ts b/src/wxBackend.ts index 716dc66be..1588ec857 100644 --- a/src/wxBackend.ts +++ b/src/wxBackend.ts @@ -36,7 +36,7 @@ import URI = require("urijs"); "use strict"; const DB_NAME = "taler"; -const DB_VERSION = 16; +const DB_VERSION = 17; import {Stores} from "./wallet"; import {Store, Index} from "./query"; @@ -226,6 +226,15 @@ function makeHandlers(db: IDBDatabase, } return wallet.getReserves(detail.exchangeBaseUrl); }, + ["get-payback-reserves"]: function (detail, sender) { + return wallet.getPaybackReserves(); + }, + ["withdraw-payback-reserve"]: function (detail, sender) { + if (typeof detail.reservePub !== "string") { + return Promise.reject(Error("reservePub missing")); + } + return wallet.withdrawPaybackReserve(detail.reservePub); + }, ["get-coins"]: function (detail, sender) { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); |