diff options
-rw-r--r-- | src/crypto/workers/cryptoApi.ts | 7 | ||||
-rw-r--r-- | src/crypto/workers/cryptoImplementation.ts | 14 | ||||
-rw-r--r-- | src/headless/taler-wallet-cli.ts | 1 | ||||
-rw-r--r-- | src/operations/exchanges.ts | 43 | ||||
-rw-r--r-- | src/operations/history.ts | 11 | ||||
-rw-r--r-- | src/operations/pending.ts | 28 | ||||
-rw-r--r-- | src/operations/recoup.ts | 372 | ||||
-rw-r--r-- | src/operations/refresh.ts | 8 | ||||
-rw-r--r-- | src/operations/reserves.ts | 1 | ||||
-rw-r--r-- | src/operations/state.ts | 1 | ||||
-rw-r--r-- | src/operations/withdraw.ts | 26 | ||||
-rw-r--r-- | src/types/dbTypes.ts | 123 | ||||
-rw-r--r-- | src/types/history.ts | 14 | ||||
-rw-r--r-- | src/types/notifications.ts | 9 | ||||
-rw-r--r-- | src/types/pending.ts | 6 | ||||
-rw-r--r-- | src/types/talerTypes.ts | 58 | ||||
-rw-r--r-- | src/types/walletTypes.ts | 6 | ||||
-rw-r--r-- | src/util/query.ts | 8 | ||||
-rw-r--r-- | src/wallet.ts | 9 | ||||
-rw-r--r-- | src/webex/messages.ts | 8 | ||||
-rw-r--r-- | src/webex/pages/payback.tsx | 40 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 113 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 9 |
23 files changed, 655 insertions, 260 deletions
diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts index 489d56f5c..4adf2882e 100644 --- a/src/crypto/workers/cryptoApi.ts +++ b/src/crypto/workers/cryptoApi.ts @@ -30,12 +30,11 @@ import { RefreshSessionRecord, TipPlanchet, WireFee, - WalletContractData, } from "../../types/dbTypes"; import { CryptoWorker } from "./cryptoWorker"; -import { ContractTerms, PaybackRequest, CoinDepositPermission } from "../../types/talerTypes"; +import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes"; import { BenchmarkResult, @@ -409,8 +408,8 @@ export class CryptoApi { return this.doRpc<boolean>("isValidWireAccount", 4, paytoUri, sig, masterPub); } - createPaybackRequest(coin: CoinRecord): Promise<PaybackRequest> { - return this.doRpc<PaybackRequest>("createPaybackRequest", 1, coin); + createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> { + return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin); } createRefreshSession( diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts index 220046209..3447c56f0 100644 --- a/src/crypto/workers/cryptoImplementation.ts +++ b/src/crypto/workers/cryptoImplementation.ts @@ -31,9 +31,10 @@ import { RefreshSessionRecord, TipPlanchet, WireFee, + CoinSourceType, } from "../../types/dbTypes"; -import { CoinDepositPermission, ContractTerms, PaybackRequest } from "../../types/talerTypes"; +import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes"; import { BenchmarkResult, PlanchetCreationResult, @@ -73,7 +74,7 @@ enum SignaturePurpose { WALLET_COIN_MELT = 1202, TEST = 4242, MERCHANT_PAYMENT_OK = 1104, - WALLET_COIN_PAYBACK = 1203, + WALLET_COIN_RECOUP = 1203, WALLET_COIN_LINK = 1204, } @@ -198,10 +199,10 @@ export class CryptoImplementation { } /** - * Create and sign a message to request payback for a coin. + * Create and sign a message to recoup a coin. */ - createPaybackRequest(coin: CoinRecord): PaybackRequest { - const p = buildSigPS(SignaturePurpose.WALLET_COIN_PAYBACK) + createRecoupRequest(coin: CoinRecord): RecoupRequest { + const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP) .put(decodeCrock(coin.coinPub)) .put(decodeCrock(coin.denomPubHash)) .put(decodeCrock(coin.blindingKey)) @@ -209,12 +210,13 @@ export class CryptoImplementation { const coinPriv = decodeCrock(coin.coinPriv); const coinSig = eddsaSign(p, coinPriv); - const paybackRequest: PaybackRequest = { + const paybackRequest: RecoupRequest = { coin_blind_key_secret: coin.blindingKey, coin_pub: coin.coinPub, coin_sig: encodeCrock(coinSig), denom_pub: coin.denomPub, denom_sig: coin.denomSig, + refreshed: (coin.coinSource.type === CoinSourceType.Refresh), }; return paybackRequest; } diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 9abdb05d1..707849952 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -365,6 +365,7 @@ advancedCli console.log(`coin ${coin.coinPub}`); console.log(` status ${coin.status}`); console.log(` exchange ${coin.exchangeBaseUrl}`); + console.log(` denomPubHash ${coin.denomPubHash}`); console.log( ` remaining amount ${Amounts.toString(coin.currentAmount)}`, ); diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts index cf6b06868..ed13a1e5b 100644 --- a/src/operations/exchanges.ts +++ b/src/operations/exchanges.ts @@ -31,6 +31,7 @@ import { WireFee, ExchangeUpdateReason, ExchangeUpdatedEventRecord, + CoinStatus, } from "../types/dbTypes"; import { canonicalizeBaseUrl } from "../util/helpers"; import * as Amounts from "../util/amounts"; @@ -45,6 +46,7 @@ import { } from "./versions"; import { getTimestampNow } from "../util/time"; import { compare } from "../util/libtoolVersion"; +import { createRecoupGroup, processRecoupGroup } from "./recoup"; async function denominationRecordFromKeys( ws: InternalWalletState, @@ -61,6 +63,7 @@ async function denominationRecordFromKeys( feeRefund: Amounts.parseOrThrow(denomIn.fee_refund), feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw), isOffered: true, + isRevoked: false, masterSig: denomIn.master_sig, stampExpireDeposit: denomIn.stamp_expire_deposit, stampExpireLegal: denomIn.stamp_expire_legal, @@ -189,6 +192,8 @@ async function updateExchangeWithKeys( ), ); + let recoupGroupId: string | undefined = undefined; + await ws.db.runWithWriteTransaction( [Stores.exchanges, Stores.denominations], async tx => { @@ -222,8 +227,46 @@ async function updateExchangeWithKeys( await tx.put(Stores.denominations, newDenom); } } + + // Handle recoup + const recoupDenomList = exchangeKeysJson.recoup ?? []; + const newlyRevokedCoinPubs: string[] = []; + for (const recoupDenomPubHash of recoupDenomList) { + const oldDenom = await tx.getIndexed( + Stores.denominations.denomPubHashIndex, + recoupDenomPubHash, + ); + if (!oldDenom) { + // We never even knew about the revoked denomination, all good. + continue; + } + if (oldDenom.isRevoked) { + // We already marked the denomination as revoked, + // this implies we revoked all coins + continue; + } + oldDenom.isRevoked = true; + await tx.put(Stores.denominations, oldDenom); + const affectedCoins = await tx + .iterIndexed(Stores.coins.denomPubIndex) + .toArray(); + for (const ac of affectedCoins) { + newlyRevokedCoinPubs.push(ac.coinPub); + } + } + if (newlyRevokedCoinPubs.length != 0) { + await createRecoupGroup(ws, tx, newlyRevokedCoinPubs); + } }, ); + + if (recoupGroupId) { + // Asynchronously start recoup. This doesn't need to finish + // for the exchange update to be considered finished. + processRecoupGroup(ws, recoupGroupId).catch((e) => { + console.log("error while recouping coins:", e); + }); + } } async function updateExchangeFinalize( diff --git a/src/operations/history.ts b/src/operations/history.ts index 2fb7854d2..2cf215a5a 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -181,6 +181,7 @@ export async function getHistory( Stores.payEvents, Stores.refundEvents, Stores.reserveUpdatedEvents, + Stores.recoupGroups, ], async tx => { tx.iter(Stores.exchanges).forEach(exchange => { @@ -485,6 +486,16 @@ export async function getHistory( amountRefundedInvalid: Amounts.toString(amountRefundedInvalid), }); }); + + tx.iter(Stores.recoupGroups).forEach(rg => { + if (rg.timestampFinished) { + history.push({ + type: HistoryEventType.FundsRecouped, + timestamp: rg.timestampFinished, + eventId: makeEventId(HistoryEventType.FundsRecouped, rg.recoupGroupId), + }); + } + }); }, ); diff --git a/src/operations/pending.ts b/src/operations/pending.ts index fce9a3bfb..08ec3fc9e 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -405,6 +405,32 @@ async function gatherPurchasePending( }); } +async function gatherRecoupPending( + tx: TransactionHandle, + now: Timestamp, + resp: PendingOperationsResponse, + onlyDue: boolean = false, +): Promise<void> { + await tx.iter(Stores.recoupGroups).forEach(rg => { + if (rg.timestampFinished) { + return; + } + resp.nextRetryDelay = updateRetryDelay( + resp.nextRetryDelay, + now, + rg.retryInfo.nextRetry, + ); + if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) { + return; + } + resp.pendingOperations.push({ + type: PendingOperationType.Recoup, + givesLifeness: true, + recoupGroupId: rg.recoupGroupId, + }); + }); +} + export async function getPendingOperations( ws: InternalWalletState, { onlyDue = false } = {}, @@ -420,6 +446,7 @@ export async function getPendingOperations( Stores.proposals, Stores.tips, Stores.purchases, + Stores.recoupGroups, ], async tx => { const walletBalance = await getBalancesInsideTransaction(ws, tx); @@ -436,6 +463,7 @@ export async function getPendingOperations( await gatherProposalPending(tx, now, resp, onlyDue); await gatherTipPending(tx, now, resp, onlyDue); await gatherPurchasePending(tx, now, resp, onlyDue); + await gatherRecoupPending(tx, now, resp, onlyDue); return resp; }, ); diff --git a/src/operations/recoup.ts b/src/operations/recoup.ts index 2b646a4d8..842a67b87 100644 --- a/src/operations/recoup.ts +++ b/src/operations/recoup.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2019 GNUnet e.V. + (C) 2019-2010 Taler Systems SA GNU 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 @@ -15,75 +15,357 @@ */ /** + * Implementation of the recoup operation, which allows to recover the + * value of coins held in a revoked denomination. + * + * @author Florian Dold <dold@taler.net> + */ + +/** * Imports. */ -import { - Database -} from "../util/query"; import { InternalWalletState } from "./state"; -import { Stores, TipRecord, CoinStatus } from "../types/dbTypes"; +import { + Stores, + CoinStatus, + CoinSourceType, + CoinRecord, + WithdrawCoinSource, + RefreshCoinSource, + ReserveRecordStatus, + RecoupGroupRecord, + initRetryInfo, + updateRetryInfoTimeout, +} from "../types/dbTypes"; -import { Logger } from "../util/logging"; -import { RecoupConfirmation, codecForRecoupConfirmation } from "../types/talerTypes"; -import { updateExchangeFromUrl } from "./exchanges"; +import { codecForRecoupConfirmation } from "../types/talerTypes"; import { NotificationType } from "../types/notifications"; +import { processReserve } from "./reserves"; -const logger = new Logger("payback.ts"); +import * as Amounts from "../util/amounts"; +import { createRefreshGroup, processRefreshGroup } from "./refresh"; +import { RefreshReason, OperationError } from "../types/walletTypes"; +import { TransactionHandle } from "../util/query"; +import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; +import { getTimestampNow } from "../util/time"; +import { guardOperationException } from "./errors"; -export async function recoup( +async function incrementRecoupRetry( ws: InternalWalletState, - coinPub: string, + recoupGroupId: string, + err: OperationError | undefined, ): Promise<void> { - let coin = await ws.db.get(Stores.coins, coinPub); - if (!coin) { - throw Error(`Coin ${coinPub} not found, can't request payback`); + await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => { + const r = await tx.get(Stores.recoupGroups, recoupGroupId); + if (!r) { + return; + } + if (!r.retryInfo) { + return; + } + r.retryInfo.retryCounter++; + updateRetryInfoTimeout(r.retryInfo); + r.lastError = err; + await tx.put(Stores.recoupGroups, r); + }); + ws.notify({ type: NotificationType.RecoupOperationError }); +} + +async function putGroupAsFinished( + tx: TransactionHandle, + recoupGroup: RecoupGroupRecord, + coinIdx: number, +): Promise<void> { + recoupGroup.recoupFinishedPerCoin[coinIdx] = true; + let allFinished = true; + for (const b of recoupGroup.recoupFinishedPerCoin) { + if (!b) { + allFinished = false; + } } - const reservePub = coin.reservePub; - if (!reservePub) { - throw Error(`Can't request payback for a refreshed coin`); + if (allFinished) { + recoupGroup.timestampFinished = getTimestampNow(); + recoupGroup.retryInfo = initRetryInfo(false); + recoupGroup.lastError = undefined; } + await tx.put(Stores.recoupGroups, recoupGroup); +} + +async function recoupTipCoin( + ws: InternalWalletState, + recoupGroupId: string, + coinIdx: number, + coin: CoinRecord, +): Promise<void> { + // We can't really recoup a coin we got via tipping. + // Thus we just put the coin to sleep. + // FIXME: somehow report this to the user + await ws.db.runWithWriteTransaction([Stores.recoupGroups], async tx => { + const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); + if (!recoupGroup) { + return; + } + if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { + return; + } + await putGroupAsFinished(tx, recoupGroup, coinIdx); + }); +} + +async function recoupWithdrawCoin( + ws: InternalWalletState, + recoupGroupId: string, + coinIdx: number, + coin: CoinRecord, + cs: WithdrawCoinSource, +): Promise<void> { + const reservePub = cs.reservePub; const reserve = await ws.db.get(Stores.reserves, reservePub); if (!reserve) { - throw Error(`Reserve of coin ${coinPub} not found`); + // FIXME: We should at least emit some pending operation / warning for this? + return; + } + + ws.notify({ + type: NotificationType.RecoupStarted, + }); + + const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); + const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); + const resp = await ws.http.postJson(reqUrl.href, recoupRequest); + if (resp.status !== 200) { + throw Error("recoup request failed"); } - switch (coin.status) { - case CoinStatus.Dormant: - throw Error(`Can't do payback for coin ${coinPub} since it's dormant`); + const recoupConfirmation = codecForRecoupConfirmation().decode( + await resp.json(), + ); + + if (recoupConfirmation.reserve_pub !== reservePub) { + throw Error(`Coin's reserve doesn't match reserve on recoup`); } - coin.status = CoinStatus.Dormant; - // 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; + + // FIXME: verify that our expectations about the amount match + await ws.db.runWithWriteTransaction( - [Stores.coins, Stores.reserves], + [Stores.coins, Stores.reserves, Stores.recoupGroups], async tx => { - await tx.put(Stores.coins, coin!!); - await tx.put(Stores.reserves, reserve); + const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); + if (!recoupGroup) { + return; + } + if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { + return; + } + const updatedCoin = await tx.get(Stores.coins, coin.coinPub); + if (!updatedCoin) { + return; + } + const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub); + if (!updatedReserve) { + return; + } + updatedCoin.status = CoinStatus.Dormant; + const currency = updatedCoin.currentAmount.currency; + updatedCoin.currentAmount = Amounts.getZero(currency); + updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; + await tx.put(Stores.coins, updatedCoin); + await tx.put(Stores.reserves, updatedReserve); + await putGroupAsFinished(tx, recoupGroup, coinIdx); }, ); + ws.notify({ - type: NotificationType.PaybackStarted, + type: NotificationType.RecoupFinished, }); - const paybackRequest = await ws.cryptoApi.createPaybackRequest(coin); - const reqUrl = new URL("payback", coin.exchangeBaseUrl); - const resp = await ws.http.postJson(reqUrl.href, paybackRequest); + processReserve(ws, reserve.reservePub).catch(e => { + console.log("processing reserve after recoup failed:", e); + }); +} + +async function recoupRefreshCoin( + ws: InternalWalletState, + recoupGroupId: string, + coinIdx: number, + coin: CoinRecord, + cs: RefreshCoinSource, +): Promise<void> { + ws.notify({ + type: NotificationType.RecoupStarted, + }); + + const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin); + const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); + const resp = await ws.http.postJson(reqUrl.href, recoupRequest); if (resp.status !== 200) { - throw Error(); + throw Error("recoup request failed"); } - const paybackConfirmation = codecForRecoupConfirmation().decode(await resp.json()); - if (paybackConfirmation.reserve_pub !== coin.reservePub) { - throw Error(`Coin's reserve doesn't match reserve on payback`); + const recoupConfirmation = codecForRecoupConfirmation().decode( + await resp.json(), + ); + + if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) { + throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`); } - coin = await ws.db.get(Stores.coins, coinPub); - if (!coin) { - throw Error(`Coin ${coinPub} not found, can't confirm payback`); + + const refreshGroupId = await ws.db.runWithWriteTransaction( + [Stores.coins, Stores.reserves], + async tx => { + const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId); + if (!recoupGroup) { + return; + } + if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { + return; + } + const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub); + const updatedCoin = await tx.get(Stores.coins, coin.coinPub); + if (!updatedCoin) { + return; + } + if (!oldCoin) { + return; + } + updatedCoin.status = CoinStatus.Dormant; + oldCoin.currentAmount = Amounts.add( + oldCoin.currentAmount, + updatedCoin.currentAmount, + ).amount; + await tx.put(Stores.coins, updatedCoin); + await putGroupAsFinished(tx, recoupGroup, coinIdx); + return await createRefreshGroup( + tx, + [{ coinPub: oldCoin.coinPub }], + RefreshReason.Recoup, + ); + }, + ); + + if (refreshGroupId) { + processRefreshGroup(ws, refreshGroupId.refreshGroupId).then(e => { + console.error("error while refreshing after recoup", e); + }); } - coin.status = CoinStatus.Dormant; - await ws.db.put(Stores.coins, coin); - ws.notify({ - type: NotificationType.PaybackFinished, +} + +async function resetRecoupGroupRetry( + ws: InternalWalletState, + recoupGroupId: string, +) { + await ws.db.mutate(Stores.recoupGroups, recoupGroupId, x => { + if (x.retryInfo.active) { + x.retryInfo = initRetryInfo(); + } + return x; }); - await updateExchangeFromUrl(ws, coin.exchangeBaseUrl, true); +} + +export async function processRecoupGroup( + ws: InternalWalletState, + recoupGroupId: string, + forceNow: boolean = false, +): Promise<void> { + await ws.memoProcessRecoup.memo(recoupGroupId, async () => { + const onOpErr = (e: OperationError) => + incrementRecoupRetry(ws, recoupGroupId, e); + return await guardOperationException( + async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow), + onOpErr, + ); + }); +} + +async function processRecoupGroupImpl( + ws: InternalWalletState, + recoupGroupId: string, + forceNow: boolean = false, +): Promise<void> { + if (forceNow) { + await resetRecoupGroupRetry(ws, recoupGroupId); + } + const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); + if (!recoupGroup) { + return; + } + if (recoupGroup.timestampFinished) { + return; + } + const ps = recoupGroup.coinPubs.map((x, i) => + processRecoup(ws, recoupGroupId, i), + ); + await Promise.all(ps); +} + +export async function createRecoupGroup( + ws: InternalWalletState, + tx: TransactionHandle, + coinPubs: string[], +): Promise<string> { + const recoupGroupId = encodeCrock(getRandomBytes(32)); + + const recoupGroup: RecoupGroupRecord = { + recoupGroupId, + coinPubs: coinPubs, + lastError: undefined, + timestampFinished: undefined, + timestampStarted: getTimestampNow(), + retryInfo: initRetryInfo(), + recoupFinishedPerCoin: coinPubs.map(() => false), + }; + + for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) { + const coinPub = coinPubs[coinIdx]; + const coin = await tx.get(Stores.coins, coinPub); + if (!coin) { + recoupGroup.recoupFinishedPerCoin[coinIdx] = true; + continue; + } + if (Amounts.isZero(coin.currentAmount)) { + recoupGroup.recoupFinishedPerCoin[coinIdx] = true; + continue; + } + coin.currentAmount = Amounts.getZero(coin.currentAmount.currency); + await tx.put(Stores.coins, coin); + } + + await tx.put(Stores.recoupGroups, recoupGroup); + + return recoupGroupId; +} + +async function processRecoup( + ws: InternalWalletState, + recoupGroupId: string, + coinIdx: number, +): Promise<void> { + const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId); + if (!recoupGroup) { + return; + } + if (recoupGroup.timestampFinished) { + return; + } + if (recoupGroup.recoupFinishedPerCoin[coinIdx]) { + return; + } + + const coinPub = recoupGroup.coinPubs[coinIdx]; + + let coin = await ws.db.get(Stores.coins, coinPub); + if (!coin) { + throw Error(`Coin ${coinPub} not found, can't request payback`); + } + + const cs = coin.coinSource; + + switch (cs.type) { + case CoinSourceType.Tip: + return recoupTipCoin(ws, recoupGroupId, coinIdx, coin); + case CoinSourceType.Refresh: + return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs); + case CoinSourceType.Withdraw: + return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs); + default: + throw Error("unknown coin source type"); + } } diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index 6dd16d61a..092d9f154 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -26,6 +26,7 @@ import { initRetryInfo, updateRetryInfoTimeout, RefreshGroupRecord, + CoinSourceType, } from "../types/dbTypes"; import { amountToPretty } from "../util/helpers"; import { Database, TransactionHandle } from "../util/query"; @@ -407,10 +408,11 @@ async function refreshReveal( denomPubHash: denom.denomPubHash, denomSig, exchangeBaseUrl: refreshSession.exchangeBaseUrl, - reservePub: undefined, status: CoinStatus.Fresh, - coinIndex: -1, - withdrawSessionId: "", + coinSource: { + type: CoinSourceType.Refresh, + oldCoinPub: refreshSession.meltCoinPub, + } }; coins.push(coin); diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 1f9cc3053..c909555fe 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -103,7 +103,6 @@ export async function createReserve( amountWithdrawCompleted: Amounts.getZero(currency), amountWithdrawRemaining: Amounts.getZero(currency), exchangeBaseUrl: canonExchange, - hasPayback: false, amountInitiallyRequested: req.amount, reservePriv: keypair.priv, reservePub: keypair.pub, diff --git a/src/operations/state.ts b/src/operations/state.ts index 3e4936c98..ae32db2b3 100644 --- a/src/operations/state.ts +++ b/src/operations/state.ts @@ -39,6 +39,7 @@ export class InternalWalletState { > = new AsyncOpMemoSingle(); memoGetBalance: AsyncOpMemoSingle<WalletBalance> = new AsyncOpMemoSingle(); memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); + memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap(); cryptoApi: CryptoApi; listeners: NotificationListener[] = []; diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 0c58f5f2f..478aa4ceb 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -24,6 +24,7 @@ import { PlanchetRecord, initRetryInfo, updateRetryInfoTimeout, + CoinSourceType, } from "../types/dbTypes"; import * as Amounts from "../util/amounts"; import { @@ -48,6 +49,7 @@ import { timestampCmp, timestampSubtractDuraction, } from "../util/time"; +import { Store } from "../util/query"; const logger = new Logger("withdraw.ts"); @@ -229,10 +231,13 @@ async function processPlanchet( denomPubHash: planchet.denomPubHash, denomSig, exchangeBaseUrl: withdrawalSession.exchangeBaseUrl, - reservePub: planchet.reservePub, status: CoinStatus.Fresh, - coinIndex: coinIdx, - withdrawSessionId: withdrawalSessionId, + coinSource: { + type: CoinSourceType.Withdraw, + coinIndex: coinIdx, + reservePub: planchet.reservePub, + withdrawSessionId: withdrawalSessionId + } }; let withdrawSessionFinished = false; @@ -449,14 +454,15 @@ async function processWithdrawCoin( return; } - const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [ - withdrawalSessionId, - coinIndex, - ]); + const planchet = withdrawalSession.planchets[coinIndex]; - if (coin) { - console.log("coin already exists"); - return; + if (planchet) { + const coin = await ws.db.get(Stores.coins, planchet.coinPub); + + if (coin) { + console.log("coin already exists"); + return; + } } if (!withdrawalSession.planchets[coinIndex]) { diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index c1d049179..56c1f82eb 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -33,10 +33,7 @@ import { } from "./talerTypes"; import { Index, Store } from "../util/query"; -import { - OperationError, - RefreshReason, -} from "./walletTypes"; +import { OperationError, RefreshReason } from "./walletTypes"; import { ReserveTransaction } from "./ReserveTransaction"; import { Timestamp, Duration, getTimestampNow } from "../util/time"; @@ -133,7 +130,6 @@ export function initRetryInfo( return info; } - /** * A reserve record as stored in the wallet's database. */ @@ -197,12 +193,6 @@ export interface ReserveRecord { amountInitiallyRequested: AmountJson; /** - * We got some payback to this reserve. We'll cease to automatically - * withdraw money from it. - */ - hasPayback: boolean; - - /** * Wire information (as payto URI) for the bank account that * transfered funds for this reserve. */ @@ -386,6 +376,8 @@ export interface DenominationRecord { /** * Did we verify the signature on the denomination? + * + * FIXME: Rename to "verificationStatus"? */ status: DenominationStatus; @@ -397,6 +389,13 @@ export interface DenominationRecord { isOffered: boolean; /** + * Did the exchange revoke the denomination? + * When this field is set to true in the database, the same transaction + * should also mark all affected coins as revoked. + */ + isRevoked: boolean; + + /** * Base URL of the exchange. */ exchangeBaseUrl: string; @@ -577,7 +576,7 @@ export interface RefreshPlanchetRecord { /** * Status of a coin. */ -export enum CoinStatus { +export const enum CoinStatus { /** * Withdrawn and never shown to anybody. */ @@ -588,26 +587,47 @@ export enum CoinStatus { Dormant = "dormant", } -export enum CoinSource { +export const enum CoinSourceType { Withdraw = "withdraw", Refresh = "refresh", Tip = "tip", } +export interface WithdrawCoinSource { + type: CoinSourceType.Withdraw; + withdrawSessionId: string; + + /** + * Index of the coin in the withdrawal session. + */ + coinIndex: number; + + /** + * Reserve public key for the reserve we got this coin from. + */ + reservePub: string; +} + +export interface RefreshCoinSource { + type: CoinSourceType.Refresh; + oldCoinPub: string; +} + +export interface TipCoinSource { + type: CoinSourceType.Tip; +} + +export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; + /** * CoinRecord as stored in the "coins" data store * of the wallet database. */ export interface CoinRecord { /** - * Withdraw session ID, or "" (empty string) if withdrawn via refresh. + * Where did the coin come from? Used for recouping coins. */ - withdrawSessionId: string; - - /** - * Index of the coin in the withdrawal session. - */ - coinIndex: number; + coinSource: CoinSource; /** * Public key of the coin. @@ -659,12 +679,6 @@ export interface CoinRecord { blindingKey: string; /** - * Reserve public key for the reserve we got this coin from, - * or zero when we got the coin from refresh. - */ - reservePub: string | undefined; - - /** * Status of the coin. */ status: CoinStatus; @@ -992,10 +1006,10 @@ export interface WireFee { /** * Record to store information about a refund event. - * + * * All information about a refund is stored with the purchase, * this event is just for the history. - * + * * The event is only present for completed refunds. */ export interface RefundEventRecord { @@ -1285,6 +1299,11 @@ export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve; export interface WithdrawalSessionRecord { withdrawSessionId: string; + /** + * Withdrawal source. Fields that don't apply to the respective + * withdrawal source type must be null (i.e. can't be absent), + * otherwise the IndexedDB indexing won't like us. + */ source: WithdrawalSource; exchangeBaseUrl: string; @@ -1343,6 +1362,46 @@ export interface BankWithdrawUriRecord { reservePub: string; } +/** + * Status of recoup operations that were grouped together. + * + * The remaining amount of involved coins should be set to zero + * in the same transaction that inserts the RecoupGroupRecord. + */ +export interface RecoupGroupRecord { + /** + * Unique identifier for the recoup group record. + */ + recoupGroupId: string; + + timestampStarted: Timestamp; + + timestampFinished: Timestamp | undefined; + + /** + * Public keys that identify the coins being recouped + * as part of this session. + * + * (Structured like this to enable multiEntry indexing in IndexedDB.) + */ + coinPubs: string[]; + + /** + * Array of flags to indicate whether the recoup finished on each individual coin. + */ + recoupFinishedPerCoin: boolean[]; + + /** + * Retry info. + */ + retryInfo: RetryInfo; + + /** + * Last error that occured, if any. + */ + lastError: OperationError | undefined; +} + export const enum ImportPayloadType { CoreSchema = "core-schema", } @@ -1398,11 +1457,6 @@ export namespace Stores { "denomPubIndex", "denomPub", ); - byWithdrawalWithIdx = new Index<any, CoinRecord>( - this, - "planchetsByWithdrawalWithIdxIndex", - ["withdrawSessionId", "coinIndex"], - ); } class ProposalsStore extends Store<ProposalRecord> { @@ -1540,6 +1594,9 @@ export namespace Stores { export const refreshGroups = new Store<RefreshGroupRecord>("refreshGroups", { keyPath: "refreshGroupId", }); + export const recoupGroups = new Store<RecoupGroupRecord>("recoupGroups", { + keyPath: "recoupGroupId", + }); export const reserves = new ReservesStore(); export const purchases = new PurchasesStore(); export const tips = new TipsStore(); diff --git a/src/types/history.ts b/src/types/history.ts index 30fe8e529..f4a1d0631 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -348,19 +348,7 @@ export interface HistoryFundsDepositedToSelfEvent { * converted funds in these denominations to new funds. */ export interface HistoryFundsRecoupedEvent { - type: HistoryEventType.FundsDepositedToSelf; - - exchangeBaseUrl: string; - - /** - * Amount that the wallet managed to recover. - */ - amountRecouped: string; - - /** - * Amount that was lost due to fees. - */ - amountLost: string; + type: HistoryEventType.FundsRecouped; } /** diff --git a/src/types/notifications.ts b/src/types/notifications.ts index 30ede151c..34e98fe2c 100644 --- a/src/types/notifications.ts +++ b/src/types/notifications.ts @@ -26,8 +26,8 @@ export const enum NotificationType { ProposalAccepted = "proposal-accepted", ProposalDownloaded = "proposal-downloaded", RefundsSubmitted = "refunds-submitted", - PaybackStarted = "payback-started", - PaybackFinished = "payback-finished", + RecoupStarted = "payback-started", + RecoupFinished = "payback-finished", RefreshRevealed = "refresh-revealed", RefreshMelted = "refresh-melted", RefreshStarted = "refresh-started", @@ -44,6 +44,7 @@ export const enum NotificationType { RefundFinished = "refund-finished", ExchangeOperationError = "exchange-operation-error", RefreshOperationError = "refresh-operation-error", + RecoupOperationError = "refresh-operation-error", RefundApplyOperationError = "refund-apply-error", RefundStatusOperationError = "refund-status-error", ProposalOperationError = "proposal-error", @@ -82,11 +83,11 @@ export interface RefundsSubmittedNotification { } export interface PaybackStartedNotification { - type: NotificationType.PaybackStarted; + type: NotificationType.RecoupStarted; } export interface PaybackFinishedNotification { - type: NotificationType.PaybackFinished; + type: NotificationType.RecoupFinished; } export interface RefreshMeltedNotification { diff --git a/src/types/pending.ts b/src/types/pending.ts index b86c7797b..5d732c520 100644 --- a/src/types/pending.ts +++ b/src/types/pending.ts @@ -58,6 +58,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon & | PendingTipChoiceOperation | PendingTipPickupOperation | PendingWithdrawOperation + | PendingRecoupOperation ); /** @@ -200,6 +201,11 @@ export interface PendingRefundApplyOperation { numRefundsDone: number; } +export interface PendingRecoupOperation { + type: PendingOperationType.Recoup; + recoupGroupId: string; +} + /** * Status of an ongoing withdrawal operation. */ diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts index 10ee83743..e65c82383 100644 --- a/src/types/talerTypes.ts +++ b/src/types/talerTypes.ts @@ -38,7 +38,12 @@ import { codecForBoolean, makeCodecForMap, } from "../util/codec"; -import { Timestamp, codecForTimestamp, Duration, codecForDuration } from "../util/time"; +import { + Timestamp, + codecForTimestamp, + Duration, + codecForDuration, +} from "../util/time"; /** * Denomination as found in the /keys response from the exchange. @@ -141,7 +146,7 @@ export class Auditor { /** * Request that we send to the exchange to get a payback. */ -export interface PaybackRequest { +export interface RecoupRequest { /** * Denomination public key of the coin we want to get * paid back. @@ -168,6 +173,11 @@ export interface PaybackRequest { * Signature made by the coin, authorizing the payback. */ coin_sig: string; + + /** + * Was the coin refreshed (and thus the recoup should go to the old coin)? + */ + refreshed: boolean; } /** @@ -175,9 +185,15 @@ export interface PaybackRequest { */ export class RecoupConfirmation { /** - * public key of the reserve that will receive the payback. + * Public key of the reserve that will receive the payback. */ - reserve_pub: string; + reserve_pub?: string; + + /** + * Public key of the old coin that will receive the recoup, + * provided if refreshed was true. + */ + old_coin_pub?: string; /** * How much will the exchange pay back (needed by wallet in @@ -575,7 +591,7 @@ export class TipResponse { * Element of the payback list that the * exchange gives us in /keys. */ -export class Payback { +export class Recoup { /** * The hash of the denomination public key for which the payback is offered. */ @@ -607,9 +623,9 @@ export class ExchangeKeysJson { list_issue_date: Timestamp; /** - * List of paybacks for compromised denominations. + * List of revoked denominations. */ - payback?: Payback[]; + recoup?: Recoup[]; /** * Short-lived signing keys used to sign online @@ -764,7 +780,10 @@ export const codecForAuditor = () => makeCodecForObject<Auditor>() .property("auditor_pub", codecForString) .property("auditor_url", codecForString) - .property("denomination_keys", makeCodecForList(codecForAuditorDenomSig())) + .property( + "denomination_keys", + makeCodecForList(codecForAuditorDenomSig()), + ) .build("Auditor"), ); @@ -779,7 +798,7 @@ export const codecForExchangeHandle = () => export const codecForAuditorHandle = () => typecheckedCodec<AuditorHandle>( makeCodecForObject<AuditorHandle>() - .property("name", codecForString) + .property("name", codecForString) .property("master_pub", codecForString) .property("url", codecForString) .build("AuditorHandle"), @@ -851,9 +870,9 @@ export const codecForTipResponse = () => .build("TipResponse"), ); -export const codecForPayback = () => - typecheckedCodec<Payback>( - makeCodecForObject<Payback>() +export const codecForRecoup = () => + typecheckedCodec<Recoup>( + makeCodecForObject<Recoup>() .property("h_denom_pub", codecForString) .build("Payback"), ); @@ -865,13 +884,12 @@ export const codecForExchangeKeysJson = () => .property("master_public_key", codecForString) .property("auditors", makeCodecForList(codecForAuditor())) .property("list_issue_date", codecForTimestamp) - .property("payback", makeCodecOptional(makeCodecForList(codecForPayback()))) + .property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup()))) .property("signkeys", codecForAny) .property("version", codecForString) .build("KeysJson"), ); - export const codecForWireFeesJson = () => typecheckedCodec<WireFeesJson>( makeCodecForObject<WireFeesJson>() @@ -895,7 +913,10 @@ export const codecForExchangeWireJson = () => typecheckedCodec<ExchangeWireJson>( makeCodecForObject<ExchangeWireJson>() .property("accounts", makeCodecForList(codecForAccountInfo())) - .property("fees", makeCodecForMap(makeCodecForList(codecForWireFeesJson()))) + .property( + "fees", + makeCodecForMap(makeCodecForList(codecForWireFeesJson())), + ) .build("ExchangeWireJson"), ); @@ -919,13 +940,12 @@ export const codecForCheckPaymentResponse = () => .build("CheckPaymentResponse"), ); - export const codecForWithdrawOperationStatusResponse = () => typecheckedCodec<WithdrawOperationStatusResponse>( makeCodecForObject<WithdrawOperationStatusResponse>() .property("selection_done", codecForBoolean) .property("transfer_done", codecForBoolean) - .property("amount",codecForString) + .property("amount", codecForString) .property("sender_wire", makeCodecOptional(codecForString)) .property("suggested_exchange", makeCodecOptional(codecForString)) .property("confirm_transfer_url", makeCodecOptional(codecForString)) @@ -945,11 +965,11 @@ export const codecForTipPickupGetResponse = () => .build("TipPickupGetResponse"), ); - export const codecForRecoupConfirmation = () => typecheckedCodec<RecoupConfirmation>( makeCodecForObject<RecoupConfirmation>() - .property("reserve_pub", codecForString) + .property("reserve_pub", makeCodecOptional(codecForString)) + .property("old_coin_pub", makeCodecOptional(codecForString)) .property("amount", codecForString) .property("timestamp", codecForTimestamp) .property("exchange_sig", codecForString) diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index 9887474c3..c6473a9b7 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -1,6 +1,6 @@ /* - This file is part of TALER - (C) 2015-2017 GNUnet e.V. and INRIA + This file is part of GNU Taler + (C) 2015-2020 Taler Systems SA 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 @@ -20,6 +20,8 @@ * These types are defined in a separate file make tree shaking easier, since * some components use these types (via RPC) but do not depend on the wallet * code directly. + * + * @author Florian Dold <dold@taler.net> */ /** diff --git a/src/util/query.ts b/src/util/query.ts index 95ef30e1b..d08c901a4 100644 --- a/src/util/query.ts +++ b/src/util/query.ts @@ -271,6 +271,14 @@ export class TransactionHandle { return new ResultStream<T>(req); } + iterIndexed<S extends IDBValidKey,T>( + index: Index<S, T>, + key?: any, + ): ResultStream<T> { + const req = this.tx.objectStore(index.storeName).index(index.indexName).openCursor(key); + return new ResultStream<T>(req); + } + delete<T>(store: Store<T>, key: any): Promise<void> { const req = this.tx.objectStore(store.name).delete(key); return requestToPromise(req); diff --git a/src/wallet.ts b/src/wallet.ts index 23ac8490b..3b619f874 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -95,7 +95,6 @@ import { getHistory } from "./operations/history"; import { getPendingOperations } from "./operations/pending"; import { getBalances } from "./operations/balance"; import { acceptTip, getTipStatus, processTip } from "./operations/tip"; -import { recoup } from "./operations/recoup"; import { TimerGroup } from "./util/timer"; import { AsyncCondition } from "./util/promiseUtils"; import { AsyncOpMemoSingle } from "./util/asyncMemo"; @@ -113,6 +112,7 @@ import { applyRefund, } from "./operations/refund"; import { durationMin, Duration } from "./util/time"; +import { processRecoupGroup } from "./operations/recoup"; const builtinCurrencies: CurrencyRecord[] = [ { @@ -217,6 +217,9 @@ export class Wallet { case PendingOperationType.RefundApply: await processPurchaseApplyRefund(this.ws, pending.proposalId, forceNow); break; + case PendingOperationType.Recoup: + await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow); + break; default: assertUnreachable(pending); } @@ -577,10 +580,6 @@ export class Wallet { return await this.db.iter(Stores.coins).toArray(); } - async getPaybackReserves(): Promise<ReserveRecord[]> { - return await this.db.iter(Stores.reserves).filter(r => r.hasPayback); - } - /** * Stop ongoing processing. */ diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 132c8c58d..7672fcb4b 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -106,14 +106,6 @@ export interface MessageMap { request: { exchangeBaseUrl: string }; response: dbTypes.ReserveRecord[]; }; - "get-payback-reserves": { - request: {}; - response: dbTypes.ReserveRecord[]; - }; - "withdraw-payback-reserve": { - request: { reservePub: string }; - response: dbTypes.ReserveRecord[]; - }; "get-denoms": { request: { exchangeBaseUrl: string }; response: dbTypes.DenominationRecord[]; diff --git a/src/webex/pages/payback.tsx b/src/webex/pages/payback.tsx index 2601887b0..96d43ff49 100644 --- a/src/webex/pages/payback.tsx +++ b/src/webex/pages/payback.tsx @@ -25,49 +25,11 @@ */ import { ReserveRecord } from "../../types/dbTypes"; import { renderAmount, registerMountPage } from "../renderHtml"; -import { getPaybackReserves, withdrawPaybackReserve } from "../wxApi"; import * as React from "react"; import { useState } from "react"; function Payback() { - const [reserves, setReserves] = useState<ReserveRecord[] | null>(null); - - useState(() => { - const update = async () => { - const r = await getPaybackReserves(); - setReserves(r); - }; - - const port = chrome.runtime.connect(); - port.onMessage.addListener((msg: any) => { - if (msg.notify) { - console.log("got notified"); - update(); - } - }); - }); - - 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 ${renderAmount(r.amountWithdrawRemaining)}</h2> - <ul> - <li>Exchange: ${r.exchangeBaseUrl}</li> - </ul> - <button onClick={() => withdrawPaybackReserve(r.reservePub)}> - Withdraw again - </button> - </div> - ))} - </div> - ); + return <div>not implemented</div>; } registerMountPage(() => <Payback />); diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 7464b1f74..5edd1907b 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -18,7 +18,6 @@ * Interface to the wallet through WebExtension messaging. */ - /** * Imports. */ @@ -28,7 +27,6 @@ import { CurrencyRecord, DenominationRecord, ExchangeRecord, - PlanchetRecord, ReserveRecord, } from "../types/dbTypes"; import { @@ -44,7 +42,6 @@ import { import { MessageMap, MessageType } from "./messages"; - /** * Response with information about available version upgrades. */ @@ -66,7 +63,6 @@ export interface UpgradeResponse { oldDbVersion: string; } - /** * Error thrown when the function from the backend (via RPC) threw an error. */ @@ -78,19 +74,22 @@ export class WalletApiError extends Error { } } - async function callBackend<T extends MessageType>( type: T, detail: MessageMap[T]["request"], ): Promise<MessageMap[T]["response"]> { return new Promise<MessageMap[T]["response"]>((resolve, reject) => { - chrome.runtime.sendMessage({ type, detail }, (resp) => { + chrome.runtime.sendMessage({ type, detail }, resp => { if (chrome.runtime.lastError) { console.log("Error calling backend"); - reject(new Error(`Error contacting backend: chrome.runtime.lastError.message`)); + reject( + new Error( + `Error contacting backend: chrome.runtime.lastError.message`, + ), + ); } if (typeof resp === "object" && resp && resp.error) { - console.warn("response error:", resp) + console.warn("response error:", resp); const e = new WalletApiError(resp.error.message, resp.error); reject(e); } else { @@ -100,42 +99,38 @@ async function callBackend<T extends MessageType>( }); } - /** * Query the wallet for the coins that would be used to withdraw * from a given reserve. */ -export function getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise<ExchangeWithdrawDetails> { +export function getReserveCreationInfo( + baseUrl: string, + amount: AmountJson, +): Promise<ExchangeWithdrawDetails> { return callBackend("reserve-creation-info", { baseUrl, amount }); } - /** * Get all exchanges the wallet knows about. */ export function getExchanges(): Promise<ExchangeRecord[]> { - return callBackend("get-exchanges", { }); + return callBackend("get-exchanges", {}); } - /** * Get all currencies the exchange knows about. */ export function getCurrencies(): Promise<CurrencyRecord[]> { - return callBackend("get-currencies", { }); + return callBackend("get-currencies", {}); } - - /** * Get information about a specific exchange. */ export function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> { - return callBackend("exchange-info", {baseUrl}); + return callBackend("exchange-info", { baseUrl }); } - /** * Replace an existing currency record with the one given. The currency to * replace is specified inside the currency record. @@ -144,7 +139,6 @@ export function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> { return callBackend("update-currency", { currencyRecord }); } - /** * Get all reserves the wallet has at an exchange. */ @@ -152,23 +146,6 @@ export function getReserves(exchangeBaseUrl: string): Promise<ReserveRecord[]> { return callBackend("get-reserves", { exchangeBaseUrl }); } - -/** - * Get all reserves for which a payback is available. - */ -export function getPaybackReserves(): Promise<ReserveRecord[]> { - return callBackend("get-payback-reserves", { }); -} - - -/** - * Withdraw the payback that is available for a reserve. - */ -export function withdrawPaybackReserve(reservePub: string): Promise<ReserveRecord[]> { - return callBackend("withdraw-payback-reserve", { reservePub }); -} - - /** * Get all coins withdrawn from the given exchange. */ @@ -176,15 +153,15 @@ export function getCoins(exchangeBaseUrl: string): Promise<CoinRecord[]> { return callBackend("get-coins", { exchangeBaseUrl }); } - /** * Get all denoms offered by the given exchange. */ -export function getDenoms(exchangeBaseUrl: string): Promise<DenominationRecord[]> { +export function getDenoms( + exchangeBaseUrl: string, +): Promise<DenominationRecord[]> { return callBackend("get-denoms", { exchangeBaseUrl }); } - /** * Start refreshing a coin. */ @@ -192,15 +169,16 @@ export function refresh(coinPub: string): Promise<void> { return callBackend("refresh-coin", { coinPub }); } - /** * Pay for a proposal. */ -export function confirmPay(proposalId: string, sessionId: string | undefined): Promise<ConfirmPayResult> { +export function confirmPay( + proposalId: string, + sessionId: string | undefined, +): Promise<ConfirmPayResult> { return callBackend("confirm-pay", { proposalId, sessionId }); } - /** * Mark a reserve as confirmed. */ @@ -212,13 +190,17 @@ export function confirmReserve(reservePub: string): Promise<void> { * Check upgrade information */ export function checkUpgrade(): Promise<UpgradeResponse> { - return callBackend("check-upgrade", { }); + return callBackend("check-upgrade", {}); } /** * Create a reserve. */ -export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: string }): Promise<any> { +export function createReserve(args: { + amount: AmountJson; + exchange: string; + senderWire?: string; +}): Promise<any> { return callBackend("create-reserve", args); } @@ -226,42 +208,45 @@ export function createReserve(args: { amount: AmountJson, exchange: string, send * Reset database */ export function resetDb(): Promise<void> { - return callBackend("reset-db", { }); + return callBackend("reset-db", {}); } /** * Get balances for all currencies/exchanges. */ export function getBalance(): Promise<WalletBalance> { - return callBackend("balances", { }); + return callBackend("balances", {}); } - /** * Get possible sender wire infos for getting money * wired from an exchange. */ export function getSenderWireInfos(): Promise<SenderWireInfos> { - return callBackend("get-sender-wire-infos", { }); + return callBackend("get-sender-wire-infos", {}); } /** * Return coins to a bank account. */ -export function returnCoins(args: { amount: AmountJson, exchange: string, senderWire: object }): Promise<void> { +export function returnCoins(args: { + amount: AmountJson; + exchange: string; + senderWire: object; +}): Promise<void> { return callBackend("return-coins", args); } - /** * Look up a purchase in the wallet database from * the contract terms hash. */ -export function getPurchaseDetails(contractTermsHash: string): Promise<PurchaseDetails> { +export function getPurchaseDetails( + contractTermsHash: string, +): Promise<PurchaseDetails> { return callBackend("get-purchase-details", { contractTermsHash }); } - /** * Get the status of processing a tip. */ @@ -276,7 +261,6 @@ export function acceptTip(talerTipUri: string): Promise<void> { return callBackend("accept-tip", { talerTipUri }); } - /** * Download a refund and accept it. */ @@ -291,7 +275,6 @@ export function abortFailedPayment(contractTermsHash: string) { return callBackend("abort-failed-payment", { contractTermsHash }); } - /** * Abort a failed payment and try to get a refund. */ @@ -302,8 +285,14 @@ export function benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> { /** * Get details about a withdraw operation. */ -export function getWithdrawDetails(talerWithdrawUri: string, maybeSelectedExchange: string | undefined) { - return callBackend("get-withdraw-details", { talerWithdrawUri, maybeSelectedExchange }); +export function getWithdrawDetails( + talerWithdrawUri: string, + maybeSelectedExchange: string | undefined, +) { + return callBackend("get-withdraw-details", { + talerWithdrawUri, + maybeSelectedExchange, + }); } /** @@ -316,8 +305,14 @@ export function preparePay(talerPayUri: string) { /** * Get details about a withdraw operation. */ -export function acceptWithdrawal(talerWithdrawUri: string, selectedExchange: string) { - return callBackend("accept-withdrawal", { talerWithdrawUri, selectedExchange }); +export function acceptWithdrawal( + talerWithdrawUri: string, + selectedExchange: string, +) { + return callBackend("accept-withdrawal", { + talerWithdrawUri, + selectedExchange, + }); } /** diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index faf917f86..248e6dfba 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -148,15 +148,6 @@ async function handleMessage( } return needsWallet().getReserves(detail.exchangeBaseUrl); } - case "get-payback-reserves": { - return needsWallet().getPaybackReserves(); - } - case "withdraw-payback-reserve": { - if (typeof detail.reservePub !== "string") { - return Promise.reject(Error("reservePub missing")); - } - throw Error("not implemented"); - } case "get-coins": { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); |