From ab724bdbd2059484335211662b63a9ae415a270c Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 4 Apr 2024 20:48:19 +0200 Subject: wallet-core: allow peer-pull with coins locked behind refresh --- .../taler-wallet-core/src/pay-peer-pull-debit.ts | 145 ++++++++++++++++----- 1 file changed, 114 insertions(+), 31 deletions(-) (limited to 'packages/taler-wallet-core/src/pay-peer-pull-debit.ts') diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts index 9bfa14ca2..705317eb6 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -37,6 +37,7 @@ import { PreparePeerPullDebitRequest, PreparePeerPullDebitResponse, RefreshReason, + SelectedProspectiveCoin, TalerError, TalerErrorCode, TalerPreciseTimestamp, @@ -427,8 +428,88 @@ async function processPeerPullDebitPendingDeposit( const pursePub = peerPullInc.pursePub; const coinSel = peerPullInc.coinSel; + if (!coinSel) { - throw Error("invalid state, no coins selected"); + const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount); + + const coinSelRes = await selectPeerCoins(wex, { + instructedAmount, + }); + if (logger.shouldLogTrace()) { + logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); + } + + let coins: SelectedProspectiveCoin[] | undefined = undefined; + + switch (coinSelRes.type) { + case "failure": + throw TalerError.fromDetail( + TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, + { + insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, + }, + ); + case "prospective": + throw Error("insufficient balance (locked behind refresh)"); + case "success": + coins = coinSelRes.result.coins; + break; + default: + assertUnreachable(coinSelRes); + } + + const peerPullDebitId = peerPullInc.peerPullDebitId; + const totalAmount = await getTotalPeerPaymentCost(wex, coins); + + // FIXME: Missing notification here! + + const transitionDone = await wex.db.runReadWriteTx( + [ + "exchanges", + "coins", + "denominations", + "refreshGroups", + "refreshSessions", + "peerPullDebit", + "coinAvailability", + ], + async (tx) => { + const pi = await tx.peerPullDebit.get(peerPullDebitId); + if (!pi) { + return false; + } + if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { + return false; + } + if (pi.coinSel) { + return false; + } + await spendCoins(wex, tx, { + // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`, + allocationId: constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullDebitId, + }), + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPull, + }); + pi.coinSel = { + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => x.contribution), + totalCost: Amounts.stringify(totalAmount), + }; + await tx.peerPullDebit.put(pi); + return true; + }, + ); + if (transitionDone) { + return TaskRunResult.progress(); + } else { + return TaskRunResult.backoff(); + } } const coins = await queryCoinInfosForSelection(wex, coinSel); @@ -595,8 +676,6 @@ export async function confirmPeerPullDebit( const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount); - // FIXME: Select coins once with pending coins, once without. - const coinSelRes = await selectPeerCoins(wex, { instructedAmount, }); @@ -604,6 +683,8 @@ export async function confirmPeerPullDebit( logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); } + let coins: SelectedProspectiveCoin[] | undefined = undefined; + switch (coinSelRes.type) { case "failure": throw TalerError.fromDetail( @@ -613,19 +694,18 @@ export async function confirmPeerPullDebit( }, ); case "prospective": - throw Error("insufficient balance (blocked on refresh)"); + coins = coinSelRes.result.prospectiveCoins; + break; case "success": + coins = coinSelRes.result.coins; break; default: assertUnreachable(coinSelRes); } - const sel = coinSelRes.result; + const totalAmount = await getTotalPeerPaymentCost(wex, coins); - const totalAmount = await getTotalPeerPaymentCost( - wex, - coinSelRes.result.coins, - ); + // FIXME: Missing notification here! await wex.db.runReadWriteTx( [ @@ -638,31 +718,33 @@ export async function confirmPeerPullDebit( "coinAvailability", ], async (tx) => { - await spendCoins(wex, tx, { - // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`, - allocationId: constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }), - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayPeerPull, - }); - const pi = await tx.peerPullDebit.get(peerPullDebitId); if (!pi) { throw Error(); } - if (pi.status === PeerPullDebitRecordStatus.DialogProposed) { - pi.status = PeerPullDebitRecordStatus.PendingDeposit; + if (pi.status !== PeerPullDebitRecordStatus.DialogProposed) { + return; + } + if (coinSelRes.type == "success") { + await spendCoins(wex, tx, { + // allocationId: `txn:peer-pull-debit:${req.peerPullDebitId}`, + allocationId: constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullDebitId, + }), + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPull, + }); pi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => x.contribution), totalCost: Amounts.stringify(totalAmount), }; } + pi.status = PeerPullDebitRecordStatus.PendingDeposit; await tx.peerPullDebit.put(pi); }, ); @@ -788,6 +870,8 @@ export async function preparePeerPullDebit( logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); } + let coins: SelectedProspectiveCoin[] | undefined = undefined; + switch (coinSelRes.type) { case "failure": throw TalerError.fromDetail( @@ -797,17 +881,16 @@ export async function preparePeerPullDebit( }, ); case "prospective": - throw Error("insufficient balance (waiting on refresh)"); + coins = coinSelRes.result.prospectiveCoins; + break; case "success": + coins = coinSelRes.result.coins; break; default: assertUnreachable(coinSelRes); } - const totalAmount = await getTotalPeerPaymentCost( - wex, - coinSelRes.result.coins, - ); + const totalAmount = await getTotalPeerPaymentCost(wex, coins); await wex.db.runReadWriteTx( ["peerPullDebit", "contractTerms"], -- cgit v1.2.3