From e7a966f755e78bf9ec200f29e49706045d1e1a54 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 4 Apr 2024 15:43:12 +0200 Subject: wallet-core: allow peer-push with coins locked behind refresh --- .../taler-wallet-core/src/pay-peer-push-debit.ts | 177 +++++++++++++++------ 1 file changed, 128 insertions(+), 49 deletions(-) (limited to 'packages/taler-wallet-core/src/pay-peer-push-debit.ts') diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts index 51b865b99..b6771be89 100644 --- a/packages/taler-wallet-core/src/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -26,6 +26,7 @@ import { Logger, NotificationType, RefreshReason, + SelectedProspectiveCoin, TalerError, TalerErrorCode, TalerPreciseTimestamp, @@ -38,6 +39,7 @@ import { TransactionState, TransactionType, assertUnreachable, + checkDbInvariant, checkLogicInvariant, encodeCrock, getRandomBytes, @@ -345,8 +347,8 @@ export async function checkPeerPushDebit( ); const coinSelRes = await selectPeerCoins(wex, { instructedAmount, - includePendingCoins: true, }); + let coins: SelectedProspectiveCoin[] | undefined = undefined; switch (coinSelRes.type) { case "failure": throw TalerError.fromDetail( @@ -356,17 +358,16 @@ export async function checkPeerPushDebit( }, ); case "prospective": - throw Error("not supported"); + coins = coinSelRes.result.prospectiveCoins; + break; case "success": + coins = coinSelRes.result.coins; break; default: assertUnreachable(coinSelRes); } - logger.trace(`selected peer coins (len=${coinSelRes.result.coins.length})`); - const totalAmount = await getTotalPeerPaymentCost( - wex, - coinSelRes.result.coins, - ); + logger.trace(`selected peer coins (len=${coins.length})`); + const totalAmount = await getTotalPeerPaymentCost(wex, coins); logger.trace("computed total peer payment cost"); return { exchangeBaseUrl: coinSelRes.result.exchangeBaseUrl, @@ -401,6 +402,8 @@ async function handlePurseCreationConflict( const instructedAmount = Amounts.parseOrThrow(peerPushInitiation.amount); const sel = peerPushInitiation.coinSel; + checkDbInvariant(!!sel); + const repair: PreviousPayCoins = []; for (let i = 0; i < sel.coinPubs.length; i++) { @@ -415,7 +418,6 @@ async function handlePurseCreationConflict( const coinSelRes = await selectPeerCoins(wex, { instructedAmount, repair, - includePendingCoins: false, }); switch (coinSelRes.type) { @@ -479,6 +481,75 @@ async function processPeerPushDebitCreateReserve( ); } + if (!peerPushInitiation.coinSel) { + const coinSelRes = await selectPeerCoins(wex, { + instructedAmount: Amounts.parseOrThrow(peerPushInitiation.amount), + }); + + switch (coinSelRes.type) { + case "failure": + throw TalerError.fromDetail( + TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE, + { + insufficientBalanceDetails: coinSelRes.insufficientBalanceDetails, + }, + ); + case "prospective": + throw Error("insufficient funds (blocked on refresh)"); + case "success": + break; + default: + assertUnreachable(coinSelRes); + } + const transitionDone = await wex.db.runReadWriteTx( + [ + "exchanges", + "contractTerms", + "coins", + "coinAvailability", + "denominations", + "refreshGroups", + "refreshSessions", + "peerPushDebit", + ], + async (tx) => { + const ppi = await tx.peerPushDebit.get(pursePub); + if (!ppi) { + return false; + } + if (ppi.coinSel) { + return false; + } + + ppi.coinSel = { + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => x.contribution), + }; + // FIXME: Instead of directly doing a spendCoin here, + // we might want to mark the coins as used and spend them + // after we've been able to create the purse. + await spendCoins(wex, tx, { + allocationId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }), + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPush, + }); + + await tx.peerPushDebit.put(ppi); + return true; + }, + ); + if (transitionDone) { + return TaskRunResult.progress(); + } + return TaskRunResult.backoff(); + } + const purseSigResp = await wex.cryptoApi.signPurseCreation({ hContractTerms, mergePub: peerPushInitiation.mergePub, @@ -625,6 +696,10 @@ async function processPeerPushDebitAbortingDeletePurse( const oldTxState = computePeerPushDebitTransactionState(ppiRec); const coinPubs: CoinRefreshRequest[] = []; + if (!ppiRec.coinSel) { + return undefined; + } + for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { coinPubs.push({ amount: ppiRec.coinSel.contributions[i], @@ -859,23 +934,26 @@ async function processPeerPushDebitReady( const oldTxState = computePeerPushDebitTransactionState(ppiRec); const coinPubs: CoinRefreshRequest[] = []; - for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { - coinPubs.push({ - amount: ppiRec.coinSel.contributions[i], - coinPub: ppiRec.coinSel.coinPubs[i], - }); + if (ppiRec.coinSel) { + for (let i = 0; i < ppiRec.coinSel.coinPubs.length; i++) { + coinPubs.push({ + amount: ppiRec.coinSel.contributions[i], + coinPub: ppiRec.coinSel.coinPubs[i], + }); + } + + const refresh = await createRefreshGroup( + wex, + tx, + currency, + coinPubs, + RefreshReason.AbortPeerPushDebit, + transactionId, + ); + + ppiRec.abortRefreshGroupId = refresh.refreshGroupId; } - - const refresh = await createRefreshGroup( - wex, - tx, - currency, - coinPubs, - RefreshReason.AbortPeerPushDebit, - transactionId, - ); ppiRec.status = PeerPushDebitStatus.AbortingRefreshExpired; - ppiRec.abortRefreshGroupId = refresh.refreshGroupId; await tx.peerPushDebit.put(ppiRec); const newTxState = computePeerPushDebitTransactionState(ppiRec); return { @@ -954,12 +1032,12 @@ export async function initiatePeerPushDebit( const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({}); - // FIXME: Check first if possible with pending coins, in that case defer coin selection const coinSelRes = await selectPeerCoins(wex, { instructedAmount, - includePendingCoins: false, }); + let coins: SelectedProspectiveCoin[] | undefined = undefined; + switch (coinSelRes.type) { case "failure": throw TalerError.fromDetail( @@ -969,8 +1047,10 @@ export async function initiatePeerPushDebit( }, ); case "prospective": - throw Error("blocked on pending refresh"); + coins = coinSelRes.result.prospectiveCoins; + break; case "success": + coins = coinSelRes.result.coins; break; default: assertUnreachable(coinSelRes); @@ -981,10 +1061,7 @@ export async function initiatePeerPushDebit( logger.info(`selected p2p coins (push):`); logger.trace(`${j2s(coinSelRes)}`); - const totalAmount = await getTotalPeerPaymentCost( - wex, - coinSelRes.result.coins, - ); + const totalAmount = await getTotalPeerPaymentCost(wex, coins); logger.info(`computed total peer payment cost`); @@ -1008,21 +1085,6 @@ export async function initiatePeerPushDebit( "peerPushDebit", ], async (tx) => { - // FIXME: Instead of directly doing a spendCoin here, - // we might want to mark the coins as used and spend them - // after we've been able to create the purse. - await spendCoins(wex, tx, { - allocationId: constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: pursePair.pub, - }), - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayPeerPush, - }); - const ppi: PeerPushDebitRecord = { amount: Amounts.stringify(instructedAmount), contractPriv: contractKeyPair.priv, @@ -1037,13 +1099,30 @@ export async function initiatePeerPushDebit( timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), status: PeerPushDebitStatus.PendingCreatePurse, contractEncNonce, - coinSel: { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - }, totalCost: Amounts.stringify(totalAmount), }; + if (coinSelRes.type === "success") { + ppi.coinSel = { + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => x.contribution), + }; + // FIXME: Instead of directly doing a spendCoin here, + // we might want to mark the coins as used and spend them + // after we've been able to create the purse. + await spendCoins(wex, tx, { + allocationId: constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: pursePair.pub, + }), + coinPubs: coinSelRes.result.coins.map((x) => x.coinPub), + contributions: coinSelRes.result.coins.map((x) => + Amounts.parseOrThrow(x.contribution), + ), + refreshReason: RefreshReason.PayPeerPush, + }); + } + await tx.peerPushDebit.add(ppi); await tx.contractTerms.put({ -- cgit v1.2.3