From bafb52edff4d56bcb9e3c3d0a260f507c517b08c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 16 Dec 2020 17:59:04 +0100 Subject: don't store reserve history anymore, adjust withdrawal implementation accordingly --- .../taler-wallet-core/src/operations/backup.ts | 41 ++- .../taler-wallet-core/src/operations/exchanges.ts | 1 - packages/taler-wallet-core/src/operations/pay.ts | 2 + .../taler-wallet-core/src/operations/pending.ts | 3 +- .../taler-wallet-core/src/operations/refresh.ts | 7 +- .../taler-wallet-core/src/operations/reserves.ts | 367 +++++++-------------- packages/taler-wallet-core/src/operations/state.ts | 2 +- packages/taler-wallet-core/src/operations/tip.ts | 16 +- .../src/operations/transactions.ts | 2 +- .../src/operations/withdraw-test.ts | 4 +- .../taler-wallet-core/src/operations/withdraw.ts | 67 ++-- 11 files changed, 205 insertions(+), 307 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/backup.ts b/packages/taler-wallet-core/src/operations/backup.ts index 7ab908c46..1e5aa542d 100644 --- a/packages/taler-wallet-core/src/operations/backup.ts +++ b/packages/taler-wallet-core/src/operations/backup.ts @@ -44,6 +44,7 @@ import { BackupRefundState, BackupReserve, BackupTip, + BackupWithdrawalGroup, WalletBackupContentV1, } from "../types/backupTypes"; import { TransactionHandle } from "../util/query"; @@ -172,6 +173,7 @@ export async function exportBackup( Stores.tips, Stores.recoupGroups, Stores.reserves, + Stores.withdrawalGroups, ], async (tx) => { const bs = await getWalletBackupState(ws, tx); @@ -188,9 +190,46 @@ export async function exportBackup( const backupBackupProviders: BackupBackupProvider[] = []; const backupTips: BackupTip[] = []; const backupRecoupGroups: BackupRecoupGroup[] = []; + const withdrawalGroupsByReserve: { + [reservePub: string]: BackupWithdrawalGroup[]; + } = {}; + + await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wg) => { + const withdrawalGroups = (withdrawalGroupsByReserve[ + wg.reservePub + ] ??= []); + // FIXME: finish! + // withdrawalGroups.push({ + // raw_withdrawal_amount: Amounts.stringify(wg.rawWithdrawalAmount), + // selected_denoms: wg.denomsSel.selectedDenoms.map((x) => ({ + // count: x.count, + // denom_pub_hash: x.denomPubHash, + // })), + // timestamp_start: wg.timestampStart, + // timestamp_finish: wg.timestampFinish, + // withdrawal_group_id: wg.withdrawalGroupId, + // }); + }); await tx.iter(Stores.reserves).forEach((reserve) => { - // FIXME: implement + const backupReserve: BackupReserve = { + initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map( + (x) => ({ + count: x.count, + denom_pub_hash: x.denomPubHash, + }), + ), + initial_withdrawal_group_id: reserve.initialWithdrawalGroupId, + instructed_amount: Amounts.stringify(reserve.instructedAmount), + reserve_priv: reserve.reservePriv, + timestamp_created: reserve.timestampCreated, + withdrawal_groups: + withdrawalGroupsByReserve[reserve.reservePub] ?? [], + }; + const backupReserves = (backupReservesByExchange[ + reserve.exchangeBaseUrl + ] ??= []); + backupReserves.push(backupReserve); }); await tx.iter(Stores.tips).forEach((tip) => { diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts index b6865cccc..3e71634cd 100644 --- a/packages/taler-wallet-core/src/operations/exchanges.ts +++ b/packages/taler-wallet-core/src/operations/exchanges.ts @@ -58,7 +58,6 @@ import { } from "../util/http"; import { Logger } from "../util/logging"; import { URL } from "../util/url"; -import { reconcileReserveHistory } from "../util/reserveHistoryUtil"; import { checkDbInvariant } from "../util/invariants"; import { NotificationType } from "../types/notifications"; import { updateRetryInfoTimeout, initRetryInfo } from "../util/retries"; diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 52f0c4510..c374cfe4a 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -460,6 +460,8 @@ async function recordConfirmPay( paymentSubmitPending: true, refunds: {}, merchantPaySig: undefined, + noncePriv: proposal.noncePriv, + noncePub: proposal.noncePub, }; await ws.db.runWithWriteTransaction( diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index a42d89c9a..cc693a49d 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -29,7 +29,7 @@ import { PendingOperationType, ExchangeUpdateOperationStage, ReserveType, -} from "../types/pending"; +} from "../types/pendingTypes"; import { Duration, getTimestampNow, @@ -189,7 +189,6 @@ async function gatherReservePending( // nothing to report as pending break; case ReserveRecordStatus.WAIT_CONFIRM_BANK: - case ReserveRecordStatus.WITHDRAWING: case ReserveRecordStatus.QUERYING_STATUS: case ReserveRecordStatus.REGISTERING_BANK: resp.nextRetryDelay = updateRetryDelay( diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 71cc78fa9..2d80f0a50 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -29,7 +29,7 @@ import { amountToPretty } from "../util/helpers"; import { TransactionHandle } from "../util/query"; import { InternalWalletState, EXCHANGE_COINS_LOCK } from "./state"; import { Logger } from "../util/logging"; -import { getWithdrawDenomList, isWithdrawableDenom } from "./withdraw"; +import { selectWithdrawalDenominations, isWithdrawableDenom } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { TalerErrorDetails, @@ -83,7 +83,7 @@ export function getTotalRefreshCost( ): AmountJson { const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh) .amount; - const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms); + const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms); const resultingAmount = Amounts.add( Amounts.getZero(withdrawAmount.currency), ...withdrawDenoms.selectedDenoms.map( @@ -150,7 +150,7 @@ async function refreshCreateSession( oldDenom.feeRefresh, ).amount; - const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms); + const newCoinDenoms = selectWithdrawalDenominations(availableAmount, availableDenoms); if (newCoinDenoms.selectedDenoms.length === 0) { logger.trace( @@ -478,6 +478,7 @@ async function refreshReveal( oldCoinPub: refreshGroup.oldCoinPubs[coinIndex], }, suspended: false, + coinEvHash: pc.coinEv, }; coins.push(coin); diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index a2a1b3018..95c38120c 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -28,8 +28,6 @@ import { CurrencyRecord, Stores, WithdrawalGroupRecord, - WalletReserveHistoryItemType, - ReserveHistoryRecord, ReserveBankInfo, } from "../types/dbTypes"; import { Logger } from "../util/logging"; @@ -47,10 +45,12 @@ import { assertUnreachable } from "../util/assertUnreachable"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { randomBytes } from "../crypto/primitives/nacl-fast"; import { - selectWithdrawalDenoms, processWithdrawGroup, getBankWithdrawalInfo, denomSelectionInfoToState, + updateWithdrawalDenoms, + selectWithdrawalDenominations, + getPossibleWithdrawalDenoms, } from "./withdraw"; import { guardOperationException, @@ -66,11 +66,6 @@ import { durationMin, durationMax, } from "../util/time"; -import { - reconcileReserveHistory, - summarizeReserveHistory, - ReserveHistorySummary, -} from "../util/reserveHistoryUtil"; import { TransactionHandle } from "../util/query"; import { addPaytoQueryParams } from "../util/payto"; import { TalerErrorCode } from "../TalerErrorCode"; @@ -86,6 +81,7 @@ import { getRetryDuration, updateRetryInfoTimeout, } from "../util/retries"; +import { ReserveTransactionType } from "../types/ReserveTransaction"; const logger = new Logger("reserves.ts"); @@ -138,11 +134,9 @@ export async function createReserve( const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32)); - const denomSelInfo = await selectWithdrawalDenoms( - ws, - canonExchange, - req.amount, - ); + await updateWithdrawalDenoms(ws, canonExchange); + const denoms = await getPossibleWithdrawalDenoms(ws, canonExchange); + const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms); const initialDenomSel = denomSelectionInfoToState(denomSelInfo); const reserveRecord: ReserveRecord = { @@ -166,16 +160,6 @@ export async function createReserve( requestedQuery: false, }; - const reserveHistoryRecord: ReserveHistoryRecord = { - reservePub: keypair.pub, - reserveTransactions: [], - }; - - reserveHistoryRecord.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Credit, - expectedAmount: req.amount, - }); - const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); const exchangeDetails = exchangeInfo.details; if (!exchangeDetails) { @@ -206,12 +190,7 @@ export async function createReserve( const cr: CurrencyRecord = currencyRecord; const resp = await ws.db.runWithWriteTransaction( - [ - Stores.currencies, - Stores.reserves, - Stores.reserveHistory, - Stores.bankWithdrawUris, - ], + [Stores.currencies, Stores.reserves, Stores.bankWithdrawUris], async (tx) => { // Check if we have already created a reserve for that bankWithdrawStatusUrl if (reserveRecord.bankInfo?.statusUrl) { @@ -238,7 +217,6 @@ export async function createReserve( } await tx.put(Stores.currencies, cr); await tx.put(Stores.reserves, reserveRecord); - await tx.put(Stores.reserveHistory, reserveHistoryRecord); const r: CreateReserveResponse = { exchange: canonExchange, reservePub: keypair.pub, @@ -499,6 +477,10 @@ async function incrementReserveRetry( /** * Update the information about a reserve that is stored in the wallet * by quering the reserve's exchange. + * + * If the reserve have funds that are not allocated in a withdrawal group yet + * and are big enough to withdraw with available denominations, + * create a new withdrawal group for the remaining amount. */ async function updateReserve( ws: InternalWalletState, @@ -542,78 +524,130 @@ async function updateReserve( } const reserveInfo = result.response; - const balance = Amounts.parseOrThrow(reserveInfo.balance); const currency = balance.currency; - let updateSummary: ReserveHistorySummary | undefined; - await ws.db.runWithWriteTransaction( - [Stores.reserves, Stores.reserveHistory], + + await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); + const denoms = await getPossibleWithdrawalDenoms(ws, reserve.exchangeBaseUrl); + + const newWithdrawalGroup = await ws.db.runWithWriteTransaction( + [Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves], async (tx) => { - const r = await tx.get(Stores.reserves, reservePub); - if (!r) { - return; - } - if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { + const newReserve = await tx.get(Stores.reserves, reserve.reservePub); + if (!newReserve) { return; } + let amountReservePlus = Amounts.getZero(currency); + let amountReserveMinus = Amounts.getZero(currency); + + // Subtract withdrawal groups for this reserve from the available amount. + await tx + .iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub) + .forEach((wg) => { + const cost = wg.denomsSel.totalWithdrawCost; + amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount; + }); - const hist = await tx.get(Stores.reserveHistory, reservePub); - if (!hist) { - throw Error("inconsistent database"); + for (const entry of reserveInfo.history) { + switch (entry.type) { + case ReserveTransactionType.Credit: + amountReservePlus = Amounts.add( + amountReservePlus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Recoup: + amountReservePlus = Amounts.add( + amountReservePlus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Closing: + amountReserveMinus = Amounts.add( + amountReserveMinus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + case ReserveTransactionType.Withdraw: { + // Now we check if the withdrawal transaction + // is part of any withdrawal known to this wallet. + const planchet = await tx.getIndexed( + Stores.planchets.coinEvHashIndex, + entry.h_coin_envelope, + ); + if (planchet) { + // Amount is already accounted in some withdrawal session + break; + } + const coin = await tx.getIndexed( + Stores.coins.coinEvHashIndex, + entry.h_coin_envelope, + ); + if (coin) { + // Amount is already accounted in some withdrawal session + break; + } + // Amount has been claimed by some withdrawal we don't know about + amountReserveMinus = Amounts.add( + amountReserveMinus, + Amounts.parseOrThrow(entry.amount), + ).amount; + break; + } + } } - const newHistoryTransactions = reserveInfo.history.slice( - hist.reserveTransactions.length, + const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus) + .amount; + const denomSelInfo = selectWithdrawalDenominations( + remainingAmount, + denoms, ); - const reserveUpdateId = encodeCrock(getRandomBytes(32)); + if (denomSelInfo.selectedDenoms.length > 0) { + let withdrawalGroupId: string; - const reconciled = reconcileReserveHistory( - hist.reserveTransactions, - reserveInfo.history, - ); - - updateSummary = summarizeReserveHistory( - reconciled.updatedLocalHistory, - currency, - ); - - if ( - reconciled.newAddedItems.length + reconciled.newMatchedItems.length != - 0 - ) { - logger.trace("setting reserve status to 'withdrawing' after query"); - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - r.retryInfo = initRetryInfo(); - r.requestedQuery = false; - } else { - if (r.requestedQuery) { - logger.trace( - "setting reserve status to 'querying-status' (requested query) after query", - ); - r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - r.requestedQuery = false; - r.retryInfo = initRetryInfo(); + if (!newReserve.initialWithdrawalStarted) { + withdrawalGroupId = newReserve.initialWithdrawalGroupId; + newReserve.initialWithdrawalStarted = true; } else { - logger.trace("setting reserve status to 'dormant' after query"); - r.reserveStatus = ReserveRecordStatus.DORMANT; - r.retryInfo = initRetryInfo(false); + withdrawalGroupId = encodeCrock(randomBytes(32)); } + + const withdrawalRecord: WithdrawalGroupRecord = { + withdrawalGroupId: withdrawalGroupId, + exchangeBaseUrl: reserve.exchangeBaseUrl, + reservePub: reserve.reservePub, + rawWithdrawalAmount: remainingAmount, + timestampStart: getTimestampNow(), + retryInfo: initRetryInfo(), + lastError: undefined, + denomsSel: denomSelectionInfoToState(denomSelInfo), + }; + + newReserve.lastError = undefined; + newReserve.retryInfo = initRetryInfo(false); + newReserve.reserveStatus = ReserveRecordStatus.DORMANT; + + await tx.put(Stores.reserves, newReserve); + await tx.put(Stores.withdrawalGroups, withdrawalRecord); + return withdrawalRecord; } - r.lastSuccessfulStatusQuery = getTimestampNow(); - hist.reserveTransactions = reconciled.updatedLocalHistory; - r.lastError = undefined; - await tx.put(Stores.reserves, r); - await tx.put(Stores.reserveHistory, hist); + return; }, ); - ws.notify({ type: NotificationType.ReserveUpdated, updateSummary }); - const reserve2 = await ws.db.get(Stores.reserves, reservePub); - if (reserve2) { - logger.trace( - `after db transaction, reserve status is ${reserve2.reserveStatus}`, - ); + + if (newWithdrawalGroup) { + logger.trace("processing new withdraw group"); + ws.notify({ + type: NotificationType.WithdrawGroupCreated, + withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId, + }); + await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId); + } else { + console.trace("withdraw session already existed"); } + return { ready: true }; } @@ -651,9 +685,6 @@ async function processReserveImpl( break; } } - case ReserveRecordStatus.WITHDRAWING: - await depleteReserve(ws, reservePub); - break; case ReserveRecordStatus.DORMANT: // nothing to do break; @@ -669,166 +700,6 @@ async function processReserveImpl( } } -/** - * Withdraw coins from a reserve until it is empty. - * - * When finished, marks the reserve as depleted by setting - * the depleted timestamp. - */ -async function depleteReserve( - ws: InternalWalletState, - reservePub: string, -): Promise { - let reserve: ReserveRecord | undefined; - let hist: ReserveHistoryRecord | undefined; - await ws.db.runWithReadTransaction( - [Stores.reserves, Stores.reserveHistory], - async (tx) => { - reserve = await tx.get(Stores.reserves, reservePub); - hist = await tx.get(Stores.reserveHistory, reservePub); - }, - ); - - if (!reserve) { - return; - } - if (!hist) { - throw Error("inconsistent database"); - } - if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return; - } - logger.trace(`depleting reserve ${reservePub}`); - - const summary = summarizeReserveHistory( - hist.reserveTransactions, - reserve.currency, - ); - - const withdrawAmount = summary.unclaimedReserveAmount; - - const denomsForWithdraw = await selectWithdrawalDenoms( - ws, - reserve.exchangeBaseUrl, - withdrawAmount, - ); - if (!denomsForWithdraw) { - // Only complain about inability to withdraw if we - // didn't withdraw before. - if (Amounts.isZero(summary.withdrawnAmount)) { - const opErr = makeErrorDetails( - TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, - `Unable to withdraw from reserve, no denominations are available to withdraw.`, - {}, - ); - await incrementReserveRetry(ws, reserve.reservePub, opErr); - throw new OperationFailedAndReportedError(opErr); - } - return; - } - - logger.trace( - `Selected coins total cost ${Amounts.stringify( - denomsForWithdraw.totalWithdrawCost, - )} for withdrawal of ${Amounts.stringify(withdrawAmount)}`, - ); - - logger.trace("selected denominations"); - - const newWithdrawalGroup = await ws.db.runWithWriteTransaction( - [ - Stores.withdrawalGroups, - Stores.reserves, - Stores.reserveHistory, - Stores.planchets, - ], - async (tx) => { - const newReserve = await tx.get(Stores.reserves, reservePub); - if (!newReserve) { - return false; - } - if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return false; - } - const newHist = await tx.get(Stores.reserveHistory, reservePub); - if (!newHist) { - throw Error("inconsistent database"); - } - const newSummary = summarizeReserveHistory( - newHist.reserveTransactions, - newReserve.currency, - ); - if ( - Amounts.cmp( - newSummary.unclaimedReserveAmount, - denomsForWithdraw.totalWithdrawCost, - ) < 0 - ) { - // Something must have happened concurrently! - logger.error( - "aborting withdrawal session, likely concurrent withdrawal happened", - ); - logger.error( - `unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`, - ); - logger.error( - `withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`, - ); - return false; - } - for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) { - const sd = denomsForWithdraw.selectedDenoms[i]; - for (let j = 0; j < sd.count; j++) { - const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount; - newHist.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Withdraw, - expectedAmount: amt, - }); - } - } - logger.trace("setting reserve status to dormant after depletion"); - newReserve.reserveStatus = ReserveRecordStatus.DORMANT; - newReserve.retryInfo = initRetryInfo(false); - - let withdrawalGroupId: string; - - if (!newReserve.initialWithdrawalStarted) { - withdrawalGroupId = newReserve.initialWithdrawalGroupId; - newReserve.initialWithdrawalStarted = true; - } else { - withdrawalGroupId = encodeCrock(randomBytes(32)); - } - - const withdrawalRecord: WithdrawalGroupRecord = { - withdrawalGroupId: withdrawalGroupId, - exchangeBaseUrl: newReserve.exchangeBaseUrl, - reservePub: newReserve.reservePub, - rawWithdrawalAmount: withdrawAmount, - timestampStart: getTimestampNow(), - retryInfo: initRetryInfo(), - lastError: undefined, - denomsSel: denomSelectionInfoToState(denomsForWithdraw), - }; - - await tx.put(Stores.reserves, newReserve); - await tx.put(Stores.reserveHistory, newHist); - await tx.put(Stores.withdrawalGroups, withdrawalRecord); - return withdrawalRecord; - }, - ); - - if (newWithdrawalGroup) { - logger.trace("processing new withdraw group"); - ws.notify({ - type: NotificationType.WithdrawGroupCreated, - withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId, - }); - await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId); - } else { - console.trace("withdraw session already existed"); - } -} - export async function createTalerWithdrawReserve( ws: InternalWalletState, talerWithdrawUri: string, diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts index c4d5b38f1..11695f6d0 100644 --- a/packages/taler-wallet-core/src/operations/state.ts +++ b/packages/taler-wallet-core/src/operations/state.ts @@ -19,7 +19,7 @@ import { BalancesResponse } from "../types/walletTypes"; import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo"; import { Logger } from "../util/logging"; -import { PendingOperationsResponse } from "../types/pending"; +import { PendingOperationsResponse } from "../types/pendingTypes"; import { WalletNotification } from "../types/notifications"; import { Database } from "../util/query"; import { openPromise, OpenedPromise } from "../util/promiseUtils"; diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index bc10e346d..f47f76623 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -32,8 +32,10 @@ import { } from "../types/dbTypes"; import { getExchangeWithdrawalInfo, - selectWithdrawalDenoms, denomSelectionInfoToState, + updateWithdrawalDenoms, + getPossibleWithdrawalDenoms, + selectWithdrawalDenominations, } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; @@ -92,12 +94,15 @@ export async function prepareTip( ); const walletTipId = encodeCrock(getRandomBytes(32)); - const selectedDenoms = await selectWithdrawalDenoms( - ws, - tipPickupStatus.exchange_url, + await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url); + const denoms = await getPossibleWithdrawalDenoms(ws, tipPickupStatus.exchange_url); + const selectedDenoms = await selectWithdrawalDenominations( amount, + denoms ); + const secretSeed = encodeCrock(getRandomBytes(64)); + tipRecord = { walletTipId: walletTipId, acceptedTimestamp: undefined, @@ -105,7 +110,6 @@ export async function prepareTip( tipExpiration: tipPickupStatus.expiration, exchangeBaseUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, - planchets: undefined, createdTimestamp: getTimestampNow(), merchantTipId: res.merchantTipId, tipAmountEffective: Amounts.sub( @@ -117,6 +121,7 @@ export async function prepareTip( lastError: undefined, denomsSel: denomSelectionInfoToState(selectedDenoms), pickedUpTimestamp: undefined, + secretSeed, }; await ws.db.put(Stores.tips, tipRecord); } @@ -316,6 +321,7 @@ async function processTipImpl( exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, suspended: false, + coinEvHash: planchet.coinEvHash, }); } diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 56e07a426..cf524db4e 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -36,7 +36,7 @@ import { WithdrawalType, WithdrawalDetails, OrderShortInfo, -} from "../types/transactions"; +} from "../types/transactionsTypes"; import { getFundingPaytoUris } from "./reserves"; import { TipResponse } from "../types/talerTypes"; diff --git a/packages/taler-wallet-core/src/operations/withdraw-test.ts b/packages/taler-wallet-core/src/operations/withdraw-test.ts index 24cb6f4b1..d21119c8c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw-test.ts +++ b/packages/taler-wallet-core/src/operations/withdraw-test.ts @@ -15,7 +15,7 @@ */ import test from "ava"; -import { getWithdrawDenomList } from "./withdraw"; +import { selectWithdrawalDenominations } from "./withdraw"; import { Amounts } from "../util/amounts"; test("withdrawal selection bug repro", (t) => { @@ -322,7 +322,7 @@ test("withdrawal selection bug repro", (t) => { }, ]; - const res = getWithdrawDenomList(amount, denoms); + const res = selectWithdrawalDenominations(amount, denoms); console.error("cost", Amounts.stringify(res.totalWithdrawCost)); console.error("withdraw amount", Amounts.stringify(amount)); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index a3bb9724c..758b80787 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -86,12 +86,13 @@ export function isWithdrawableDenom(d: DenominationRecord): boolean { return started && stillOkay && !d.isRevoked; } + /** * Get a list of denominations (with repetitions possible) * whose total value is as close as possible to the available * amount, but never larger. */ -export function getWithdrawDenomList( +export function selectWithdrawalDenominations( amountAvailable: AmountJson, denoms: DenominationRecord[], ): DenominationSelectionInfo { @@ -207,7 +208,7 @@ export async function getBankWithdrawalInfo( /** * Return denominations that can potentially used for a withdrawal. */ -async function getPossibleDenoms( +export async function getPossibleWithdrawalDenoms( ws: InternalWalletState, exchangeBaseUrl: string, ): Promise { @@ -470,6 +471,7 @@ async function processPlanchetVerifyAndStoreCoin( denomPub: planchet.denomPub, denomPubHash: planchet.denomPubHash, denomSig, + coinEvHash: planchet.coinEvHash, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, status: CoinStatus.Fresh, coinSource: { @@ -524,17 +526,13 @@ export function denomSelectionInfoToState( } /** - * Get a list of denominations to withdraw from the given exchange for the - * given amount, making sure that all denominations' signatures are verified. - * - * Writes to the DB in order to record the result from verifying - * denominations. + * Make sure that denominations that currently can be used for withdrawal + * are validated, and the result of validation is stored in the database. */ -export async function selectWithdrawalDenoms( +export async function updateWithdrawalDenoms( ws: InternalWalletState, exchangeBaseUrl: string, - amount: AmountJson, -): Promise { +): Promise { const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl); if (!exchange) { logger.error("exchange not found"); @@ -545,43 +543,24 @@ export async function selectWithdrawalDenoms( logger.error("exchange details not available"); throw Error(`exchange ${exchangeBaseUrl} details not available`); } - - let allValid = false; - let selectedDenoms: DenominationSelectionInfo; - - // Find a denomination selection for the requested amount. - // If a selected denomination has not been validated yet - // and turns our to be invalid, we try again with the - // reduced set of denominations. - do { - allValid = true; - const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl); - selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms); - for (const denomSel of selectedDenoms.selectedDenoms) { - const denom = denomSel.denom; - if (denom.status === DenominationStatus.Unverified) { - const valid = await ws.cryptoApi.isValidDenom( - denom, - exchangeDetails.masterPublicKey, - ); - if (!valid) { - denom.status = DenominationStatus.VerifiedBad; - allValid = false; - } else { - denom.status = DenominationStatus.VerifiedGood; - } - await ws.db.put(Stores.denominations, denom); + const denominations = await getPossibleWithdrawalDenoms(ws, exchangeBaseUrl); + for (const denom of denominations) { + if (denom.status === DenominationStatus.Unverified) { + const valid = await ws.cryptoApi.isValidDenom( + denom, + exchangeDetails.masterPublicKey, + ); + if (!valid) { + denom.status = DenominationStatus.VerifiedBad; + } else { + denom.status = DenominationStatus.VerifiedGood; } + await ws.db.put(Stores.denominations, denom); } - } while (selectedDenoms.selectedDenoms.length > 0 && !allValid); - - if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) { - throw Error("Bug: withdrawal coin selection is wrong"); } - - return selectedDenoms; } + async function incrementWithdrawalRetry( ws: InternalWalletState, withdrawalGroupId: string, @@ -745,7 +724,9 @@ export async function getExchangeWithdrawalInfo( throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`); } - const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount); + await updateWithdrawalDenoms(ws, baseUrl); + const denoms = await getPossibleWithdrawalDenoms(ws, baseUrl); + const selectedDenoms = selectWithdrawalDenominations(amount, denoms); const exchangeWireAccounts: string[] = []; for (const account of exchangeWireInfo.accounts) { exchangeWireAccounts.push(account.payto_uri); -- cgit v1.2.3