From c021876b41bff11ad28c3a43808795fa0d02ce99 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 14 Sep 2022 20:34:37 +0200 Subject: wallet-core: cache fresh coin count in DB --- .../src/operations/backup/import.ts | 1 - .../taler-wallet-core/src/operations/deposits.ts | 20 +++--- packages/taler-wallet-core/src/operations/pay.ts | 77 ++++------------------ .../src/operations/peer-to-peer.ts | 46 +++++-------- .../taler-wallet-core/src/operations/refresh.ts | 23 +++++-- packages/taler-wallet-core/src/operations/tip.ts | 6 +- .../taler-wallet-core/src/operations/withdraw.ts | 7 +- 7 files changed, 61 insertions(+), 119 deletions(-) (limited to 'packages/taler-wallet-core/src/operations') diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 8f5d019d4..53e45918e 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -413,7 +413,6 @@ export async function importBackup( currentAmount: Amounts.parseOrThrow(backupCoin.current_amount), denomSig: backupCoin.denom_sig, coinPub: compCoin.coinPub, - suspended: false, exchangeBaseUrl: backupExchangeDetails.base_url, denomPubHash, status: backupCoin.fresh diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 5838be765..6d63def59 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -33,12 +33,11 @@ import { getRandomBytes, hashWire, Logger, - NotificationType, parsePaytoUri, PayCoinSelection, PrepareDepositRequest, PrepareDepositResponse, - TalerErrorDetail, + RefreshReason, TalerProtocolTimestamp, TrackDepositGroupRequest, TrackDepositGroupResponse, @@ -46,18 +45,15 @@ import { } from "@gnu-taler/taler-util"; import { DepositGroupRecord, - OperationAttemptErrorResult, OperationAttemptResult, OperationStatus, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { selectPayCoins } from "../util/coinSelection.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; -import { RetryInfo } from "../util/retries.js"; -import { guardOperationException } from "./common.js"; +import { spendCoins } from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { - applyCoinSpend, CoinSelectionRequest, extractContractData, generateDepositPermissions, @@ -525,12 +521,12 @@ export async function createDepositGroup( x.refreshGroups, ]) .runReadWrite(async (tx) => { - await applyCoinSpend( - ws, - tx, - payCoinSel, - `deposit-group:${depositGroup.depositGroupId}`, - ); + await spendCoins(ws, tx, { + allocationId: `deposit-group:${depositGroup.depositGroupId}`, + coinPubs: payCoinSel.coinPubs, + contributions: payCoinSel.coinContributions, + refreshReason: RefreshReason.PayDeposit, + }); await tx.depositGroups.put(depositGroup); }); diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts index 322e90487..bd7b1f7f0 100644 --- a/packages/taler-wallet-core/src/operations/pay.ts +++ b/packages/taler-wallet-core/src/operations/pay.ts @@ -100,6 +100,7 @@ import { } from "../util/http.js"; import { GetReadWriteAccess } from "../util/query.js"; import { RetryInfo, RetryTags, scheduleRetry } from "../util/retries.js"; +import { spendCoins } from "../wallet.js"; import { getExchangeDetails } from "./exchanges.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; @@ -156,9 +157,6 @@ export async function getTotalPaymentCost( } function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean { - if (coin.suspended) { - return false; - } if (denom.isRevoked) { return false; } @@ -347,65 +345,6 @@ export async function getCandidatePayCoins( }; } -/** - * Apply a coin selection to the database. Marks coins as spent - * and creates a refresh session for the remaining amount. - * - * FIXME: This does not deal well with conflicting spends! - * When two payments are made in parallel, the same coin can be selected - * for two payments. - * However, this is a situation that can also happen via sync. - */ -export async function applyCoinSpend( - ws: InternalWalletState, - tx: GetReadWriteAccess<{ - coins: typeof WalletStoresV1.coins; - refreshGroups: typeof WalletStoresV1.refreshGroups; - denominations: typeof WalletStoresV1.denominations; - }>, - coinSelection: PayCoinSelection, - allocationId: string, -): Promise { - logger.info(`applying coin spend ${j2s(coinSelection)}`); - for (let i = 0; i < coinSelection.coinPubs.length; i++) { - const coin = await tx.coins.get(coinSelection.coinPubs[i]); - if (!coin) { - throw Error("coin allocated for payment doesn't exist anymore"); - } - const contrib = coinSelection.coinContributions[i]; - if (coin.status !== CoinStatus.Fresh) { - const alloc = coin.allocation; - if (!alloc) { - continue; - } - if (alloc.id !== allocationId) { - // FIXME: assign error code - throw Error("conflicting coin allocation (id)"); - } - if (0 !== Amounts.cmp(alloc.amount, contrib)) { - // FIXME: assign error code - throw Error("conflicting coin allocation (contrib)"); - } - continue; - } - coin.status = CoinStatus.Dormant; - coin.allocation = { - id: allocationId, - amount: Amounts.stringify(contrib), - }; - const remaining = Amounts.sub(coin.currentAmount, contrib); - if (remaining.saturated) { - throw Error("not enough remaining balance on coin for payment"); - } - coin.currentAmount = remaining.amount; - await tx.coins.put(coin); - } - const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({ - coinPub: x, - })); - await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.PayMerchant); -} - /** * Record all information that is necessary to * pay for a proposal in the wallet's database. @@ -468,7 +407,12 @@ async function recordConfirmPay( await tx.proposals.put(p); } await tx.purchases.put(t); - await applyCoinSpend(ws, tx, coinSelection, `proposal:${t.proposalId}`); + await spendCoins(ws, tx, { + allocationId: `proposal:${t.proposalId}`, + coinPubs: coinSelection.coinPubs, + contributions: coinSelection.coinContributions, + refreshReason: RefreshReason.PayMerchant, + }); }); ws.notify({ @@ -1038,7 +982,12 @@ async function handleInsufficientFunds( p.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); p.coinDepositPermissions = undefined; await tx.purchases.put(p); - await applyCoinSpend(ws, tx, res, `proposal:${p.proposalId}`); + await spendCoins(ws, tx, { + allocationId: `proposal:${p.proposalId}`, + coinPubs: p.payCoinSelection.coinPubs, + contributions: p.payCoinSelection.coinContributions, + refreshReason: RefreshReason.PayMerchant, + }); }); } diff --git a/packages/taler-wallet-core/src/operations/peer-to-peer.ts b/packages/taler-wallet-core/src/operations/peer-to-peer.ts index 59dad3d55..449a91c68 100644 --- a/packages/taler-wallet-core/src/operations/peer-to-peer.ts +++ b/packages/taler-wallet-core/src/operations/peer-to-peer.ts @@ -75,6 +75,8 @@ import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { createRefreshGroup } from "./refresh.js"; import { updateExchangeFromUrl } from "./exchanges.js"; +import { spendCoins } from "../wallet.js"; +import { RetryTags } from "../util/retries.js"; const logger = new Logger("operations/peer-to-peer.ts"); @@ -256,18 +258,14 @@ export async function initiatePeerToPeerPush( return undefined; } - const pubs: CoinPublicKey[] = []; - for (const c of sel.coins) { - const coin = await tx.coins.get(c.coinPub); - checkDbInvariant(!!coin); - coin.currentAmount = Amounts.sub( - coin.currentAmount, - Amounts.parseOrThrow(c.contribution), - ).amount; - coin.status = CoinStatus.Dormant; - pubs.push({ coinPub: coin.coinPub }); - await tx.coins.put(coin); - } + await spendCoins(ws, tx, { + allocationId: `peer-push:${pursePair.pub}`, + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPush, + }); await tx.peerPushPaymentInitiations.add({ amount: Amounts.stringify(instructedAmount), @@ -284,8 +282,6 @@ export async function initiatePeerToPeerPush( timestampCreated: TalerProtocolTimestamp.now(), }); - await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPush); - return sel; }); logger.info(`selected p2p coins (push): ${j2s(coinSelRes)}`); @@ -588,20 +584,14 @@ export async function acceptPeerPullPayment( return undefined; } - const pubs: CoinPublicKey[] = []; - for (const c of sel.coins) { - const coin = await tx.coins.get(c.coinPub); - checkDbInvariant(!!coin); - coin.currentAmount = Amounts.sub( - coin.currentAmount, - Amounts.parseOrThrow(c.contribution), - ).amount; - coin.status = CoinStatus.Dormant; - pubs.push({ coinPub: coin.coinPub }); - await tx.coins.put(coin); - } - - await createRefreshGroup(ws, tx, pubs, RefreshReason.PayPeerPull); + await spendCoins(ws, tx, { + allocationId: `peer-pull:${req.peerPullPaymentIncomingId}`, + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPull, + }); const pi = await tx.peerPullPaymentIncoming.get( req.peerPullPaymentIncomingId, diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 719093bd8..d1c366cd0 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -77,6 +77,7 @@ import { import { checkDbInvariant } from "../util/invariants.js"; import { GetReadWriteAccess } from "../util/query.js"; import { RetryInfo, runOperationHandlerForResult } from "../util/retries.js"; +import { makeCoinAvailable } from "../wallet.js"; import { guardOperationException } from "./common.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { @@ -670,7 +671,6 @@ async function refreshReveal( type: CoinSourceType.Refresh, oldCoinPub: refreshGroup.oldCoinPubs[coinIndex], }, - suspended: false, coinEvHash: pc.coinEvHash, ageCommitmentProof: pc.ageCommitmentProof, }; @@ -680,7 +680,7 @@ async function refreshReveal( } await ws.db - .mktx((x) => [x.coins, x.refreshGroups]) + .mktx((x) => [x.coins, x.denominations, x.refreshGroups]) .runReadWrite(async (tx) => { const rg = await tx.refreshGroups.get(refreshGroupId); if (!rg) { @@ -694,7 +694,7 @@ async function refreshReveal( rg.statusPerCoin[coinIndex] = RefreshCoinStatus.Finished; updateGroupStatus(rg); for (const coin of coins) { - await tx.coins.put(coin); + await makeCoinAvailable(ws, tx, coin); } await tx.refreshGroups.put(rg); }); @@ -865,10 +865,22 @@ export async function createRefreshGroup( !!denom, "denomination for existing coin must be in database", ); + if (coin.status !== CoinStatus.Dormant) { + coin.status = CoinStatus.Dormant; + const denom = await tx.denominations.get([ + coin.exchangeBaseUrl, + coin.denomPubHash, + ]); + checkDbInvariant(!!denom); + checkDbInvariant( + denom.freshCoinCount != null && denom.freshCoinCount > 0, + ); + denom.freshCoinCount--; + await tx.denominations.put(denom); + } const refreshAmount = coin.currentAmount; inputPerCoin.push(refreshAmount); coin.currentAmount = Amounts.getZero(refreshAmount.currency); - coin.status = CoinStatus.Dormant; await tx.coins.put(coin); const denoms = await getDenoms(coin.exchangeBaseUrl); const cost = getTotalRefreshCost(denoms, denom, refreshAmount); @@ -965,9 +977,6 @@ export async function autoRefresh( if (coin.status !== CoinStatus.Fresh) { continue; } - if (coin.suspended) { - continue; - } const denom = await tx.denominations.get([ exchangeBaseUrl, coin.denomPubHash, diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 04da2b988..f70e2d02b 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -51,6 +51,7 @@ import { readSuccessResponseJsonOrThrow, } from "../util/http.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; +import { makeCoinAvailable } from "../wallet.js"; import { updateExchangeFromUrl } from "./exchanges.js"; import { getCandidateWithdrawalDenoms, @@ -310,13 +311,12 @@ export async function processTip( denomSig: { cipher: DenomKeyType.Rsa, rsa_signature: denomSigRsa.sig }, exchangeBaseUrl: tipRecord.exchangeBaseUrl, status: CoinStatus.Fresh, - suspended: false, coinEvHash: planchet.coinEvHash, }); } await ws.db - .mktx((x) => [x.coins, x.tips, x.withdrawalGroups]) + .mktx((x) => [x.coins, x.denominations, x.tips]) .runReadWrite(async (tx) => { const tr = await tx.tips.get(walletTipId); if (!tr) { @@ -328,7 +328,7 @@ export async function processTip( tr.pickedUpTimestamp = TalerProtocolTimestamp.now(); await tx.tips.put(tr); for (const cr of newCoinRecords) { - await tx.coins.put(cr); + await makeCoinAvailable(ws, tx, cr); } }); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 1b8383776..bee83265c 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -93,11 +93,11 @@ import { } from "../util/http.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { DbAccess, GetReadOnlyAccess } from "../util/query.js"; -import { RetryInfo } from "../util/retries.js"; import { WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION, } from "../versions.js"; +import { makeCoinAvailable } from "../wallet.js"; import { getExchangeDetails, getExchangePaytoUri, @@ -805,7 +805,6 @@ async function processPlanchetVerifyAndStoreCoin( reservePub: planchet.reservePub, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, }, - suspended: false, ageCommitmentProof: planchet.ageCommitmentProof, }; @@ -815,7 +814,7 @@ async function processPlanchetVerifyAndStoreCoin( // withdrawal succeeded. If so, mark the withdrawal // group as finished. const firstSuccess = await ws.db - .mktx((x) => [x.coins, x.withdrawalGroups, x.planchets]) + .mktx((x) => [x.coins, x.denominations, x.withdrawalGroups, x.planchets]) .runReadWrite(async (tx) => { const p = await tx.planchets.get(planchetCoinPub); if (!p || p.withdrawalDone) { @@ -823,7 +822,7 @@ async function processPlanchetVerifyAndStoreCoin( } p.withdrawalDone = true; await tx.planchets.put(p); - await tx.coins.add(coin); + await makeCoinAvailable(ws, tx, coin); return true; }); -- cgit v1.2.3