diff options
author | Florian Dold <florian@dold.me> | 2024-04-04 15:43:12 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-04-04 15:43:12 +0200 |
commit | e7a966f755e78bf9ec200f29e49706045d1e1a54 (patch) | |
tree | f6843b80fbf190b547aed3f3c37d4ae1720b5d7e /packages/taler-wallet-core/src | |
parent | ac8adbc4a1a93664763d7e35bade940d598d0f74 (diff) | |
download | wallet-core-e7a966f755e78bf9ec200f29e49706045d1e1a54.tar.xz |
wallet-core: allow peer-push with coins locked behind refresh
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.ts | 9 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/db.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/pay-peer-common.ts | 14 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/pay-peer-pull-debit.ts | 3 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/pay-peer-push-debit.ts | 177 |
5 files changed, 135 insertions, 70 deletions
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index bce51fd91..0027241c4 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -1001,13 +1001,6 @@ export interface PeerCoinSelectionRequest { * selection instead of selecting completely new coins. */ repair?: PreviousPayCoins; - - /** - * If set to true, the coin selection will also use coins that are not - * materially available yet, but that are expected to become available - * as the output of a refresh operation. - */ - includePendingCoins: boolean; } export async function computeCoinSelMaxExpirationDate( @@ -1191,7 +1184,7 @@ export async function selectPeerCoins( false, ); - if (!avRes && req.includePendingCoins) { + if (!avRes) { // Try to see if we can do a prospective selection const prospectiveAvRes = await internalSelectPeerCoins( wex, diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a675fa8dd..6db85d8f6 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1892,7 +1892,7 @@ export interface PeerPushDebitRecord { totalCost: AmountString; - coinSel: DbPeerPushPaymentCoinSelection; + coinSel?: DbPeerPushPaymentCoinSelection; contractTermsHash: HashCodeString; diff --git a/packages/taler-wallet-core/src/pay-peer-common.ts b/packages/taler-wallet-core/src/pay-peer-common.ts index 599010c1d..6ad8ecb70 100644 --- a/packages/taler-wallet-core/src/pay-peer-common.ts +++ b/packages/taler-wallet-core/src/pay-peer-common.ts @@ -22,7 +22,7 @@ import { AmountString, Amounts, Codec, - SelectedCoin, + SelectedProspectiveCoin, TalerProtocolTimestamp, buildCodecForObject, checkDbInvariant, @@ -74,21 +74,17 @@ export async function queryCoinInfosForSelection( export async function getTotalPeerPaymentCost( wex: WalletExecutionContext, - pcs: SelectedCoin[], + pcs: SelectedProspectiveCoin[], ): Promise<AmountJson> { const currency = Amounts.currencyOf(pcs[0].contribution); return wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => { const costs: AmountJson[] = []; for (let i = 0; i < pcs.length; i++) { - const coin = await tx.coins.get(pcs[i].coinPub); - if (!coin) { - throw Error("can't calculate payment cost, coin not found"); - } const denomInfo = await getDenomInfo( wex, tx, - coin.exchangeBaseUrl, - coin.denomPubHash, + pcs[i].exchangeBaseUrl, + pcs[i].denomPubHash, ); if (!denomInfo) { throw Error( @@ -98,7 +94,7 @@ export async function getTotalPeerPaymentCost( const allDenoms = await getCandidateWithdrawalDenomsTx( wex, tx, - coin.exchangeBaseUrl, + pcs[i].exchangeBaseUrl, currency, ); const amountLeft = Amounts.sub( 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 2cc241187..9bfa14ca2 100644 --- a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -373,7 +373,6 @@ async function handlePurseCreationConflict( const coinSelRes = await selectPeerCoins(ws, { instructedAmount, repair, - includePendingCoins: false, }); switch (coinSelRes.type) { @@ -600,7 +599,6 @@ export async function confirmPeerPullDebit( const coinSelRes = await selectPeerCoins(wex, { instructedAmount, - includePendingCoins: false, }); if (logger.shouldLogTrace()) { logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); @@ -785,7 +783,6 @@ export async function preparePeerPullDebit( const coinSelRes = await selectPeerCoins(wex, { instructedAmount, - includePendingCoins: true, }); if (logger.shouldLogTrace()) { logger.trace(`selected p2p coins (pull): ${j2s(coinSelRes)}`); 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({ |