From 9d660788521d93452aa767d86158889fd4870fd1 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 10 Mar 2022 16:30:24 +0100 Subject: wallet-core: do not rely on reserve history for withdrawals --- packages/taler-wallet-core/src/db.ts | 14 +-- .../src/operations/backup/import.ts | 1 - packages/taler-wallet-core/src/operations/pay.ts | 4 +- .../taler-wallet-core/src/operations/recoup.ts | 48 +++++++- .../taler-wallet-core/src/operations/refresh.ts | 3 +- .../taler-wallet-core/src/operations/reserves.ts | 87 ++++++-------- packages/taler-wallet-core/src/operations/tip.ts | 1 - .../taler-wallet-core/src/operations/withdraw.ts | 32 ++++-- .../taler-wallet-core/src/util/coinSelection.ts | 2 + packages/taler-wallet-core/src/wallet.ts | 126 +++++++++++++++------ 10 files changed, 194 insertions(+), 124 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ac28d0979..2e76ab523 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -597,9 +597,6 @@ export interface PlanchetRecord { denomPubHash: string; - // FIXME: maybe too redundant? - denomPub: DenominationPubKey; - blindingKey: string; withdrawSig: string; @@ -607,10 +604,6 @@ export interface PlanchetRecord { coinEv: CoinEnvelope; coinEvHash: string; - - coinValue: AmountJson; - - isFromTip: boolean; } /** @@ -685,11 +678,6 @@ export interface CoinRecord { */ coinPriv: string; - /** - * Key used by the exchange used to sign the coin. - */ - denomPub: DenominationPubKey; - /** * Hash of the public key that signs the coin. */ @@ -1378,6 +1366,8 @@ export interface WithdrawalGroupRecord { /** * When was the withdrawal operation completed? + * + * FIXME: We should probably drop this and introduce an OperationStatus field. */ timestampFinish?: Timestamp; diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 84acfb16c..35b62c2e4 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -419,7 +419,6 @@ export async function importBackup( coinPub: compCoin.coinPub, suspended: false, exchangeBaseUrl: backupExchangeDetails.base_url, - denomPub: backupDenomination.denom_pub, denomPubHash, status: backupCoin.fresh ? CoinStatus.Fresh diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 97d87e5cc..6001cac4f 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -315,7 +315,7 @@ export async function getCandidatePayCoins( candidateCoins.push({ availableAmount: coin.currentAmount, coinPub: coin.coinPub, - denomPub: coin.denomPub, + denomPub: denom.denomPub, feeDeposit: denom.feeDeposit, exchangeBaseUrl: denom.exchangeBaseUrl, }); @@ -1397,7 +1397,7 @@ export async function generateDepositPermissions( coinPub: coin.coinPub, contractTermsHash: contractData.contractTermsHash, denomPubHash: coin.denomPubHash, - denomKeyType: coin.denomPub.cipher, + denomKeyType: denom.denomPub.cipher, denomSig: coin.denomSig, exchangeBaseUrl: coin.exchangeBaseUrl, feeDeposit: denom.feeDeposit, diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts index afca923bd..23d14f212 100644 --- a/packages/taler-wallet-core/src/operations/recoup.ts +++ b/packages/taler-wallet-core/src/operations/recoup.ts @@ -164,18 +164,34 @@ async function recoupWithdrawCoin( cs: WithdrawCoinSource, ): Promise { const reservePub = cs.reservePub; - const reserve = await ws.db + const d = await ws.db .mktx((x) => ({ reserves: x.reserves, + denominations: x.denominations, })) .runReadOnly(async (tx) => { - return tx.reserves.get(reservePub); + const reserve = await tx.reserves.get(reservePub); + if (!reserve) { + return; + } + const denomInfo = await ws.getDenomInfo( + ws, + tx, + reserve.exchangeBaseUrl, + coin.denomPubHash, + ); + if (!denomInfo) { + return; + } + return { reserve, denomInfo }; }); - if (!reserve) { + if (!d) { // FIXME: We should at least emit some pending operation / warning for this? return; } + const { reserve, denomInfo } = d; + ws.notify({ type: NotificationType.RecoupStarted, }); @@ -184,7 +200,7 @@ async function recoupWithdrawCoin( blindingKey: coin.blindingKey, coinPriv: coin.coinPriv, coinPub: coin.coinPub, - denomPub: coin.denomPub, + denomPub: denomInfo.denomPub, denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, }); @@ -253,6 +269,28 @@ async function recoupRefreshCoin( coin: CoinRecord, cs: RefreshCoinSource, ): Promise { + const d = await ws.db + .mktx((x) => ({ + coins: x.coins, + denominations: x.denominations, + })) + .runReadOnly(async (tx) => { + const denomInfo = await ws.getDenomInfo( + ws, + tx, + coin.exchangeBaseUrl, + coin.denomPubHash, + ); + if (!denomInfo) { + return; + } + return { denomInfo }; + }); + if (!d) { + // FIXME: We should at least emit some pending operation / warning for this? + return; + } + ws.notify({ type: NotificationType.RecoupStarted, }); @@ -261,7 +299,7 @@ async function recoupRefreshCoin( blindingKey: coin.blindingKey, coinPriv: coin.coinPriv, coinPub: coin.coinPub, - denomPub: coin.denomPub, + denomPub: d.denomInfo.denomPub, denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, }); diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index ba4cb697d..550119de1 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -395,7 +395,7 @@ async function refreshMelt( oldCoin.exchangeBaseUrl, ); let meltReqBody: any; - if (oldCoin.denomPub.cipher === DenomKeyType.Rsa) { + if (oldDenom.denomPub.cipher === DenomKeyType.Rsa) { meltReqBody = { coin_pub: oldCoin.coinPub, confirm_sig: derived.confirmSig, @@ -671,7 +671,6 @@ async function refreshReveal( coinPriv: pc.coinPriv, coinPub: pc.coinPub, currentAmount: denom.value, - denomPub: denom.denomPub, denomPubHash: denom.denomPubHash, denomSig: { cipher: DenomKeyType.Rsa, diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 7011b2f26..a16d3ec31 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -587,8 +587,8 @@ async function updateReserve( logger.trace(`got reserve status ${j2s(result.response)}`); const reserveInfo = result.response; - const balance = Amounts.parseOrThrow(reserveInfo.balance); - const currency = balance.currency; + const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance); + const currency = reserveBalance.currency; await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); const denoms = await getCandidateWithdrawalDenoms( @@ -598,73 +598,50 @@ async function updateReserve( const newWithdrawalGroup = await ws.db .mktx((x) => ({ - coins: x.coins, planchets: x.planchets, withdrawalGroups: x.withdrawalGroups, reserves: x.reserves, + denominations: x.denominations, })) .runReadWrite(async (tx) => { const newReserve = await tx.reserves.get(reserve.reservePub); if (!newReserve) { return; } - let amountReservePlus = Amounts.getZero(currency); + let amountReservePlus = reserveBalance; let amountReserveMinus = Amounts.getZero(currency); - // Subtract withdrawal groups for this reserve from the available amount. + // Subtract amount allocated in unfinished withdrawal groups + // for this reserve from the available amount. await tx.withdrawalGroups.indexes.byReservePub .iter(reservePub) - .forEach((wg) => { - const cost = wg.denomsSel.totalWithdrawCost; - amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount; - }); - - 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.planchets.indexes.byCoinEvHash.get( - entry.h_coin_envelope, - ); - if (planchet) { - // Amount is already accounted in some withdrawal session - break; - } - const coin = await tx.coins.indexes.byCoinEvHash.get( - 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; + .forEachAsync(async (wg) => { + if (wg.timestampFinish) { + return; } - } - } + await tx.planchets.indexes.byGroup + .iter(wg.withdrawalGroupId) + .forEachAsync(async (pr) => { + if (pr.withdrawalDone) { + return; + } + const denomInfo = await ws.getDenomInfo( + ws, + tx, + wg.exchangeBaseUrl, + pr.denomPubHash, + ); + if (!denomInfo) { + logger.error(`no denom info found for ${pr.denomPubHash}`); + return; + } + amountReserveMinus = Amounts.add( + amountReserveMinus, + denomInfo.value, + denomInfo.feeWithdraw, + ).amount; + }); + }); const remainingAmount = Amounts.sub( amountReservePlus, diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index cc2d71ef4..a2a4e6f49 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -374,7 +374,6 @@ async function processTipImpl( walletTipId: walletTipId, }, currentAmount: denom.value, - denomPub: denom.denomPub, denomPubHash: denom.denomPubHash, denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa }, exchangeBaseUrl: tipRecord.exchangeBaseUrl, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 731e9b3aa..ae3763a02 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -418,10 +418,7 @@ async function processPlanchetGenerate( coinIdx, coinPriv: r.coinPriv, coinPub: r.coinPub, - coinValue: r.coinValue, - denomPub: r.denomPub, denomPubHash: r.denomPubHash, - isFromTip: false, reservePub: r.reservePub, withdrawalDone: false, withdrawSig: r.withdrawSig, @@ -557,6 +554,7 @@ async function processPlanchetVerifyAndStoreCoin( .mktx((x) => ({ withdrawalGroups: x.withdrawalGroups, planchets: x.planchets, + denominations: x.denominations, })) .runReadOnly(async (tx) => { let planchet = await tx.planchets.indexes.byGroupAndIndex.get([ @@ -570,16 +568,29 @@ async function processPlanchetVerifyAndStoreCoin( logger.warn("processPlanchet: planchet already withdrawn"); return; } - return { planchet, exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl }; + const denomInfo = await ws.getDenomInfo( + ws, + tx, + withdrawalGroup.exchangeBaseUrl, + planchet.denomPubHash, + ); + if (!denomInfo) { + return; + } + return { + planchet, + denomInfo, + exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl, + }; }); if (!d) { return; } - const { planchet, exchangeBaseUrl } = d; + const { planchet, denomInfo } = d; - const planchetDenomPub = planchet.denomPub; + const planchetDenomPub = denomInfo.denomPub; if (planchetDenomPub.cipher !== DenomKeyType.Rsa) { throw Error(`cipher (${planchetDenomPub.cipher}) not supported`); } @@ -623,9 +634,9 @@ async function processPlanchetVerifyAndStoreCoin( } let denomSig: UnblindedSignature; - if (planchet.denomPub.cipher === DenomKeyType.Rsa) { + if (planchetDenomPub.cipher === DenomKeyType.Rsa) { denomSig = { - cipher: planchet.denomPub.cipher, + cipher: planchetDenomPub.cipher, rsa_signature: denomSigRsa, }; } else { @@ -636,12 +647,11 @@ async function processPlanchetVerifyAndStoreCoin( blindingKey: planchet.blindingKey, coinPriv: planchet.coinPriv, coinPub: planchet.coinPub, - currentAmount: planchet.coinValue, - denomPub: planchet.denomPub, + currentAmount: denomInfo.value, denomPubHash: planchet.denomPubHash, denomSig, coinEvHash: planchet.coinEvHash, - exchangeBaseUrl: exchangeBaseUrl, + exchangeBaseUrl: d.exchangeBaseUrl, status: CoinStatus.Fresh, coinSource: { type: CoinSourceType.Withdraw, diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index 4f8a01d19..e19b58774 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -77,6 +77,8 @@ export interface AvailableCoinInfo { /** * Coin's denomination public key. + * + * FIXME: We should only need the denomPubHash here, if at all. */ denomPub: DenominationPubKey; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index ac0def3c1..329417562 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -24,23 +24,62 @@ */ import { AcceptManualWithdrawalResult, - AcceptWithdrawalResponse, AmountJson, Amounts, BalancesResponse, codecForAbortPayWithRefundRequest, + AcceptWithdrawalResponse, + AmountJson, + Amounts, + BalancesResponse, + codecForAbortPayWithRefundRequest, codecForAcceptBankIntegratedWithdrawalRequest, codecForAcceptExchangeTosRequest, codecForAcceptManualWithdrawalRequet, codecForAcceptTipRequest, - codecForAddExchangeRequest, codecForAny, codecForApplyRefundRequest, + codecForAddExchangeRequest, + codecForAny, + codecForApplyRefundRequest, codecForConfirmPayRequest, - codecForCreateDepositGroupRequest, codecForDeleteTransactionRequest, codecForForceRefreshRequest, - codecForGetExchangeTosRequest, codecForGetExchangeWithdrawalInfo, codecForGetFeeForDeposit, codecForGetWithdrawalDetailsForAmountRequest, - codecForGetWithdrawalDetailsForUri, codecForImportDbRequest, codecForIntegrationTestArgs, codecForListKnownBankAccounts, codecForPreparePayRequest, - codecForPrepareTipRequest, codecForRetryTransactionRequest, codecForSetCoinSuspendedRequest, codecForSetWalletDeviceIdRequest, codecForTestPayArgs, - codecForTrackDepositGroupRequest, codecForTransactionsRequest, codecForWithdrawFakebankRequest, codecForWithdrawTestBalance, CoinDumpJson, CoreApiResponse, durationFromSpec, - durationMin, ExchangeListItem, - ExchangesListRespose, getDurationRemaining, GetExchangeTosResult, isTimestampExpired, - j2s, KnownBankAccounts, Logger, ManualWithdrawalDetails, NotificationType, parsePaytoUri, PaytoUri, RefreshReason, TalerErrorCode, + codecForCreateDepositGroupRequest, + codecForDeleteTransactionRequest, + codecForForceRefreshRequest, + codecForGetExchangeTosRequest, + codecForGetExchangeWithdrawalInfo, + codecForGetFeeForDeposit, + codecForGetWithdrawalDetailsForAmountRequest, + codecForGetWithdrawalDetailsForUri, + codecForImportDbRequest, + codecForIntegrationTestArgs, + codecForListKnownBankAccounts, + codecForPreparePayRequest, + codecForPrepareTipRequest, + codecForRetryTransactionRequest, + codecForSetCoinSuspendedRequest, + codecForSetWalletDeviceIdRequest, + codecForTestPayArgs, + codecForTrackDepositGroupRequest, + codecForTransactionsRequest, + codecForWithdrawFakebankRequest, + codecForWithdrawTestBalance, + CoinDumpJson, + CoreApiResponse, + durationFromSpec, + durationMin, + ExchangeListItem, + ExchangesListRespose, + getDurationRemaining, + GetExchangeTosResult, + isTimestampExpired, + j2s, + KnownBankAccounts, + Logger, + ManualWithdrawalDetails, + NotificationType, + parsePaytoUri, + PaytoUri, + RefreshReason, + TalerErrorCode, Timestamp, - timestampMin, URL, WalletNotification + timestampMin, + URL, + WalletNotification, } from "@gnu-taler/taler-util"; import { DenomInfo, @@ -50,7 +89,7 @@ import { MerchantOperations, NotificationListener, RecoupOperations, - ReserveOperations + ReserveOperations, } from "./common.js"; import { CryptoApi, CryptoWorkerFactory } from "./crypto/workers/cryptoApi.js"; import { @@ -59,12 +98,12 @@ import { exportDb, importDb, ReserveRecordStatus, - WalletStoresV1 + WalletStoresV1, } from "./db.js"; import { makeErrorDetails, OperationFailedAndReportedError, - OperationFailedError + OperationFailedError, } from "./errors.js"; import { exportBackup } from "./operations/backup/export.js"; import { @@ -77,7 +116,7 @@ import { loadBackupRecovery, processBackupForProvider, removeBackupProvider, - runBackupCycle + runBackupCycle, } from "./operations/backup/index.js"; import { setWalletDeviceId } from "./operations/backup/state.js"; import { getBalances } from "./operations/balance.js"; @@ -85,7 +124,7 @@ import { createDepositGroup, getFeeForDeposit, processDepositGroup, - trackDepositGroup + trackDepositGroup, } from "./operations/deposits.js"; import { acceptExchangeTermsOfService, @@ -94,62 +133,64 @@ import { getExchangeRequestTimeout, getExchangeTrust, updateExchangeFromUrl, - updateExchangeTermsOfService + updateExchangeTermsOfService, } from "./operations/exchanges.js"; import { getMerchantInfo } from "./operations/merchants.js"; import { confirmPay, preparePayForUri, processDownloadProposal, - processPurchasePay + processPurchasePay, } from "./operations/pay.js"; import { getPendingOperations } from "./operations/pending.js"; import { createRecoupGroup, processRecoupGroup } from "./operations/recoup.js"; import { autoRefresh, createRefreshGroup, - processRefreshGroup + processRefreshGroup, } from "./operations/refresh.js"; import { abortFailedPayWithRefund, applyRefund, - processPurchaseQueryRefund + processPurchaseQueryRefund, } from "./operations/refund.js"; import { createReserve, createTalerWithdrawReserve, getFundingPaytoUris, - processReserve + processReserve, } from "./operations/reserves.js"; import { runIntegrationTest, testPay, - withdrawTestBalance + withdrawTestBalance, } from "./operations/testing.js"; import { acceptTip, prepareTip, processTip } from "./operations/tip.js"; import { deleteTransaction, getTransactions, - retryTransaction + retryTransaction, } from "./operations/transactions.js"; import { getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, - processWithdrawGroup + processWithdrawGroup, } from "./operations/withdraw.js"; import { - PendingOperationsResponse, PendingTaskInfo, PendingTaskType + PendingOperationsResponse, + PendingTaskInfo, + PendingTaskType, } from "./pending-types.js"; import { assertUnreachable } from "./util/assertUnreachable.js"; import { AsyncOpMemoMap, AsyncOpMemoSingle } from "./util/asyncMemo.js"; import { HttpRequestLibrary, - readSuccessResponseJsonOrThrow + readSuccessResponseJsonOrThrow, } from "./util/http.js"; import { AsyncCondition, OpenedPromise, - openPromise + openPromise, } from "./util/promiseUtils.js"; import { DbAccess, GetReadWriteAccess } from "./util/query.js"; import { TimerGroup } from "./util/timer.js"; @@ -455,7 +496,10 @@ async function getExchangeTos( ) { throw Error("exchange is in invalid state"); } - if (acceptedFormat && acceptedFormat.findIndex(f => f === contentType) !== -1) { + if ( + acceptedFormat && + acceptedFormat.findIndex((f) => f === contentType) !== -1 + ) { return { acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag, currentEtag, @@ -464,7 +508,12 @@ async function getExchangeTos( }; } - const tosDownload = await downloadTosFromAcceptedFormat(ws, exchangeBaseUrl, getExchangeRequestTimeout(), acceptedFormat); + const tosDownload = await downloadTosFromAcceptedFormat( + ws, + exchangeBaseUrl, + getExchangeRequestTimeout(), + acceptedFormat, + ); if (tosDownload.tosContentType === contentType) { return { @@ -474,7 +523,7 @@ async function getExchangeTos( contentType, }; } - await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload) + await updateExchangeTermsOfService(ws, exchangeBaseUrl, tosDownload); return { acceptedEtag: exchangeDetails.termsOfServiceAcceptedEtag, @@ -482,7 +531,6 @@ async function getExchangeTos( content: tosDownload.tosText, contentType: tosDownload.tosContentType, }; - } async function listKnownBankAccounts( @@ -641,9 +689,15 @@ async function dumpCoins(ws: InternalWalletState): Promise { } withdrawalReservePub = ws.reservePub; } + const denomInfo = await ws.getDenomInfo( + ws, + tx, + c.exchangeBaseUrl, + c.denomPubHash, + ); coinsJson.coins.push({ coin_pub: c.coinPub, - denom_pub: c.denomPub, + denom_pub: denomInfo?.denomPub!, denom_pub_hash: c.denomPubHash, denom_value: Amounts.stringify(denom.value), exchange_base_url: c.exchangeBaseUrl, @@ -1030,7 +1084,7 @@ export async function handleCoreApiRequest( try { logger.error("Caught unexpected exception:"); logger.error(e.stack); - } catch (e) { } + } catch (e) {} return { type: "error", operation, @@ -1236,7 +1290,10 @@ class InternalWalletStateImpl implements InternalWalletState { * Run an async function after acquiring a list of locks, identified * by string tokens. */ - async runSequentialized(tokens: string[], f: () => Promise): Promise { + async runSequentialized( + tokens: string[], + f: () => Promise, + ): Promise { // Make sure locks are always acquired in the same order tokens = [...tokens].sort(); @@ -1269,4 +1326,3 @@ class InternalWalletStateImpl implements InternalWalletState { } } } - -- cgit v1.2.3