diff options
author | Florian Dold <florian@dold.me> | 2024-04-03 16:21:33 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-04-03 16:21:33 +0200 |
commit | 65a656163797e9dd298b34ec916b982082db7f52 (patch) | |
tree | 1a226c657639c69194ddf7682a805bf2aa14191c /packages/taler-wallet-core/src/deposits.ts | |
parent | 5417b8b7b866f1c4f4d99d6ec9ad001af67822b6 (diff) | |
download | wallet-core-65a656163797e9dd298b34ec916b982082db7f52.tar.gz wallet-core-65a656163797e9dd298b34ec916b982082db7f52.tar.bz2 wallet-core-65a656163797e9dd298b34ec916b982082db7f52.zip |
wallet-core: allow deposits with balance locked behind refresh
Diffstat (limited to 'packages/taler-wallet-core/src/deposits.ts')
-rw-r--r-- | packages/taler-wallet-core/src/deposits.ts | 230 |
1 files changed, 170 insertions, 60 deletions
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index 05a5d780a..5b23d8325 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -413,16 +413,28 @@ async function refundDepositGroup( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, ): Promise<TaskRunResult> { - const newTxPerCoin = [...depositGroup.statusPerCoin]; + const statusPerCoin = depositGroup.statusPerCoin; + const payCoinSelection = depositGroup.payCoinSelection; + if (!statusPerCoin) { + throw Error( + "unable to refund deposit group without coin selection (status missing)", + ); + } + if (!payCoinSelection) { + throw Error( + "unable to refund deposit group without coin selection (selection missing)", + ); + } + const newTxPerCoin = [...statusPerCoin]; logger.info(`status per coin: ${j2s(depositGroup.statusPerCoin)}`); - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - const st = depositGroup.statusPerCoin[i]; + for (let i = 0; i < statusPerCoin.length; i++) { + const st = statusPerCoin[i]; switch (st) { case DepositElementStatus.RefundFailed: case DepositElementStatus.RefundSuccess: break; default: { - const coinPub = depositGroup.payCoinSelection.coinPubs[i]; + const coinPub = payCoinSelection.coinPubs[i]; const coinExchange = await wex.db.runReadOnlyTx( ["coins"], async (tx) => { @@ -431,7 +443,7 @@ async function refundDepositGroup( return coinRecord.exchangeBaseUrl; }, ); - const refundAmount = depositGroup.payCoinSelection.coinContributions[i]; + const refundAmount = payCoinSelection.coinContributions[i]; // We use a constant refund transaction ID, since there can // only be one refund. const rtid = 1; @@ -503,8 +515,8 @@ async function refundDepositGroup( const refreshCoins: CoinRefreshRequest[] = []; for (let i = 0; i < newTxPerCoin.length; i++) { refreshCoins.push({ - amount: depositGroup.payCoinSelection.coinContributions[i], - coinPub: depositGroup.payCoinSelection.coinPubs[i], + amount: payCoinSelection.coinContributions[i], + coinPub: payCoinSelection.coinPubs[i], }); } let refreshRes: CreateRefreshGroupResult | undefined = undefined; @@ -740,9 +752,21 @@ async function processDepositGroupPendingTrack( wex: WalletExecutionContext, depositGroup: DepositGroupRecord, ): Promise<TaskRunResult> { + const statusPerCoin = depositGroup.statusPerCoin; + const payCoinSelection = depositGroup.payCoinSelection; + if (!statusPerCoin) { + throw Error( + "unable to refund deposit group without coin selection (status missing)", + ); + } + if (!payCoinSelection) { + throw Error( + "unable to refund deposit group without coin selection (selection missing)", + ); + } const { depositGroupId } = depositGroup; - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - const coinPub = depositGroup.payCoinSelection.coinPubs[i]; + for (let i = 0; i < statusPerCoin.length; i++) { + const coinPub = payCoinSelection.coinPubs[i]; // FIXME: Make the URL part of the coin selection? const exchangeBaseUrl = await wex.db.runReadWriteTx( ["coins"], @@ -761,7 +785,7 @@ async function processDepositGroupPendingTrack( } | undefined; - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { + if (statusPerCoin[i] !== DepositElementStatus.Wired) { const track = await trackDeposit( wex, depositGroup, @@ -826,6 +850,9 @@ async function processDepositGroupPendingTrack( if (!dg) { return; } + if (!dg.statusPerCoin) { + return; + } if (updatedTxStatus !== undefined) { dg.statusPerCoin[i] = updatedTxStatus; } @@ -858,9 +885,12 @@ async function processDepositGroupPendingTrack( if (!dg) { return undefined; } + if (!dg.statusPerCoin) { + return undefined; + } const oldTxState = computeDepositTransactionStatus(dg); - for (let i = 0; i < depositGroup.statusPerCoin.length; i++) { - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.Wired) { + for (let i = 0; i < dg.statusPerCoin.length; i++) { + if (dg.statusPerCoin[i] !== DepositElementStatus.Wired) { allWired = false; break; } @@ -924,6 +954,87 @@ async function processDepositGroupPendingDeposit( // Check for cancellation before expensive operations. cancellationToken?.throwIfCancelled(); + if (!depositGroup.payCoinSelection) { + logger.info("missing coin selection for deposit group, selecting now"); + // FIXME: Consider doing the coin selection inside the txn + const payCoinSel = await selectPayCoins(wex, { + restrictExchanges: { + auditors: [], + exchanges: contractData.allowedExchanges, + }, + restrictWireMethod: contractData.wireMethod, + contractTermsAmount: Amounts.parseOrThrow(contractData.amount), + depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee), + wireFeeAmortization: 1, // FIXME #8653 + prevPayCoins: [], + }); + + switch (payCoinSel.type) { + case "success": + logger.info("coin selection success"); + break; + case "failure": + logger.info("coin selection failure"); + throw TalerError.fromDetail( + TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + { + insufficientBalanceDetails: payCoinSel.insufficientBalanceDetails, + }, + ); + case "prospective": + logger.info("coin selection prospective"); + throw Error("insufficient balance (waiting on pending refresh)"); + default: + assertUnreachable(payCoinSel); + } + + const transitionDone = await wex.db.runReadWriteTx( + [ + "depositGroups", + "coins", + "coinAvailability", + "refreshGroups", + "refreshSessions", + "denominations", + ], + async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + return false; + } + if (dg.statusPerCoin) { + return false; + } + dg.payCoinSelection = { + coinContributions: payCoinSel.coinSel.coins.map( + (x) => x.contribution, + ), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + }; + dg.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + dg.statusPerCoin = payCoinSel.coinSel.coins.map( + () => DepositElementStatus.DepositPending, + ); + await tx.depositGroups.put(dg); + await spendCoins(wex, tx, { + allocationId: transactionId, + coinPubs: dg.payCoinSelection.coinPubs, + contributions: dg.payCoinSelection.coinContributions.map((x) => + Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayDeposit, + }); + return true; + }, + ); + + if (transitionDone) { + return TaskRunResult.progress(); + } else { + return TaskRunResult.backoff(); + } + } + // FIXME: Cache these! const depositPermissions = await generateDepositPermissions( wex, @@ -990,6 +1101,9 @@ async function processDepositGroupPendingDeposit( if (!dg) { return; } + if (!dg.statusPerCoin) { + return; + } for (const batchIndex of batchIndexes) { const coinStatus = dg.statusPerCoin[batchIndex]; switch (coinStatus) { @@ -1360,8 +1474,11 @@ export async function createDepositGroup( prevPayCoins: [], }); + let coins: SelectedProspectiveCoin[] | undefined = undefined; + switch (payCoinSel.type) { case "success": + coins = payCoinSel.coinSel.coins; break; case "failure": throw TalerError.fromDetail( @@ -1371,17 +1488,13 @@ export async function createDepositGroup( }, ); case "prospective": - // FIXME: Here we need to create the deposit group without a full coin selection! - throw Error("insufficient balance (pending refresh)"); + coins = payCoinSel.result.prospectiveCoins; + break; default: assertUnreachable(payCoinSel); } - const totalDepositCost = await getTotalPaymentCost( - wex, - currency, - payCoinSel.coinSel.coins, - ); + const totalDepositCost = await getTotalPaymentCost(wex, currency, coins); let depositGroupId: string; if (req.transactionId) { @@ -1396,34 +1509,23 @@ export async function createDepositGroup( const infoPerExchange: Record<string, DepositInfoPerExchange> = {}; - await wex.db.runReadOnlyTx(["coins"], async (tx) => { - for (let i = 0; i < payCoinSel.coinSel.coins.length; i++) { - const coin = await tx.coins.get(payCoinSel.coinSel.coins[i].coinPub); - if (!coin) { - logger.error("coin not found anymore"); - continue; - } - let depPerExchange = infoPerExchange[coin.exchangeBaseUrl]; - if (!depPerExchange) { - infoPerExchange[coin.exchangeBaseUrl] = depPerExchange = { - amountEffective: Amounts.stringify( - Amounts.zeroOfAmount(totalDepositCost), - ), - }; - } - const contrib = payCoinSel.coinSel.coins[i].contribution; - depPerExchange.amountEffective = Amounts.stringify( - Amounts.add(depPerExchange.amountEffective, contrib).amount, - ); + for (let i = 0; i < coins.length; i++) { + let depPerExchange = infoPerExchange[coins[i].exchangeBaseUrl]; + if (!depPerExchange) { + infoPerExchange[coins[i].exchangeBaseUrl] = depPerExchange = { + amountEffective: Amounts.stringify( + Amounts.zeroOfAmount(totalDepositCost), + ), + }; } - }); + const contrib = coins[i].contribution; + depPerExchange.amountEffective = Amounts.stringify( + Amounts.add(depPerExchange.amountEffective, contrib).amount, + ); + } const counterpartyEffectiveDepositAmount = - await getCounterpartyEffectiveDepositAmount( - wex, - p.targetType, - payCoinSel.coinSel.coins, - ); + await getCounterpartyEffectiveDepositAmount(wex, p.targetType, coins); const depositGroup: DepositGroupRecord = { contractTermsHash, @@ -1436,14 +1538,9 @@ export async function createDepositGroup( AbsoluteTime.toPreciseTimestamp(now), ), timestampFinished: undefined, - statusPerCoin: payCoinSel.coinSel.coins.map( - () => DepositElementStatus.DepositPending, - ), - payCoinSelection: { - coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution), - coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), - }, - payCoinSelectionUid: encodeCrock(getRandomBytes(32)), + statusPerCoin: undefined, + payCoinSelection: undefined, + payCoinSelectionUid: undefined, merchantPriv: merchantPair.priv, merchantPub: merchantPair.pub, totalPayCost: Amounts.stringify(totalDepositCost), @@ -1461,6 +1558,17 @@ export async function createDepositGroup( infoPerExchange, }; + if (payCoinSel.type === "success") { + depositGroup.payCoinSelection = { + coinContributions: payCoinSel.coinSel.coins.map((x) => x.contribution), + coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), + }; + depositGroup.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + depositGroup.statusPerCoin = payCoinSel.coinSel.coins.map( + () => DepositElementStatus.DepositPending, + ); + } + const ctx = new DepositTransactionContext(wex, depositGroupId); const transactionId = ctx.transactionId; @@ -1476,14 +1584,16 @@ export async function createDepositGroup( "contractTerms", ], async (tx) => { - await spendCoins(wex, tx, { - allocationId: transactionId, - coinPubs: payCoinSel.coinSel.coins.map((x) => x.coinPub), - contributions: payCoinSel.coinSel.coins.map((x) => - Amounts.parseOrThrow(x.contribution), - ), - refreshReason: RefreshReason.PayDeposit, - }); + if (depositGroup.payCoinSelection) { + await spendCoins(wex, tx, { + allocationId: transactionId, + coinPubs: depositGroup.payCoinSelection.coinPubs, + contributions: depositGroup.payCoinSelection.coinContributions.map( + (x) => Amounts.parseOrThrow(x), + ), + refreshReason: RefreshReason.PayDeposit, + }); + } await tx.depositGroups.put(depositGroup); await tx.contractTerms.put({ contractTermsRaw: contractTerms, |