diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts | 499 |
1 files changed, 222 insertions, 277 deletions
diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts index 72e9e2e4a..9bbe2c875 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-debit.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022-2023 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -14,6 +14,15 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +/** + * @fileoverview + * Implementation of the peer-pull-debit transaction, i.e. + * paying for an invoice the wallet received from another wallet. + */ + +/** + * Imports. + */ import { AcceptPeerPullPaymentResponse, Amounts, @@ -53,19 +62,25 @@ import { readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; import { + DbReadWriteTransactionArr, InternalWalletState, PeerPullDebitRecordStatus, PeerPullPaymentIncomingRecord, PendingTaskType, RefreshOperationStatus, + StoreNames, + WalletStoresV1, createRefreshGroup, timestampPreciseToDb, } from "../index.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; +import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { TaskRunResult, TaskRunResultType, + TransactionContext, + TransitionResult, constructTaskIdentifier, spendCoins, } from "./common.js"; @@ -80,19 +95,181 @@ import { parseTransactionIdentifier, stopLongpolling, } from "./transactions.js"; -import { PeerCoinRepair, selectPeerCoins } from "../util/coinSelection.js"; const logger = new Logger("pay-peer-pull-debit.ts"); +/** + * Common context for a peer-pull-debit transaction. + */ +export class PeerPullDebitTransactionContext implements TransactionContext { + ws: InternalWalletState; + transactionId: string; + taskId: string; + peerPullDebitId: string; + + constructor(ws: InternalWalletState, peerPullDebitId: string) { + this.ws = ws; + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullDebitId, + }); + this.taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullDebit, + peerPullDebitId, + }); + this.peerPullDebitId = peerPullDebitId; + } + + async resumeTransaction(): Promise<void> { + const ctx = this; + stopLongpolling(ctx.ws, ctx.taskId); + await ctx.transition(async (pi) => { + switch (pi.status) { + case PeerPullDebitRecordStatus.SuspendedDeposit: + pi.status = PeerPullDebitRecordStatus.PendingDeposit; + return TransitionResult.Transition; + case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: + pi.status = PeerPullDebitRecordStatus.AbortingRefresh; + return TransitionResult.Transition; + case PeerPullDebitRecordStatus.Aborted: + case PeerPullDebitRecordStatus.AbortingRefresh: + case PeerPullDebitRecordStatus.Failed: + case PeerPullDebitRecordStatus.DialogProposed: + case PeerPullDebitRecordStatus.Done: + case PeerPullDebitRecordStatus.PendingDeposit: + return TransitionResult.Stay; + } + }); + } + + async failTransaction(): Promise<void> { + const ctx = this; + stopLongpolling(ctx.ws, ctx.taskId); + await ctx.transition(async (pi) => { + switch (pi.status) { + case PeerPullDebitRecordStatus.SuspendedDeposit: + case PeerPullDebitRecordStatus.PendingDeposit: + case PeerPullDebitRecordStatus.AbortingRefresh: + case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: + // FIXME: Should we also abort the corresponding refresh session?! + pi.status = PeerPullDebitRecordStatus.Failed; + return TransitionResult.Transition; + default: + return TransitionResult.Stay; + } + }); + } + + async abortTransaction(): Promise<void> { + const ctx = this; + await ctx.transitionExtra( + { + extraStores: [ + "coinAvailability", + "denominations", + "refreshGroups", + "coins", + "coinAvailability", + ], + }, + async (pi, tx) => { + switch (pi.status) { + case PeerPullDebitRecordStatus.SuspendedDeposit: + case PeerPullDebitRecordStatus.PendingDeposit: + break; + default: + return TransitionResult.Stay; + } + const currency = Amounts.currencyOf(pi.totalCostEstimated); + const coinPubs: CoinRefreshRequest[] = []; + + if (!pi.coinSel) { + throw Error("invalid db state"); + } + + for (let i = 0; i < pi.coinSel.coinPubs.length; i++) { + coinPubs.push({ + amount: pi.coinSel.contributions[i], + coinPub: pi.coinSel.coinPubs[i], + }); + } + + const refresh = await createRefreshGroup( + ctx.ws, + tx, + currency, + coinPubs, + RefreshReason.AbortPeerPullDebit, + ); + + pi.status = PeerPullDebitRecordStatus.AbortingRefresh; + pi.abortRefreshGroupId = refresh.refreshGroupId; + return TransitionResult.Transition; + }, + ); + } + + async transition( + f: (rec: PeerPullPaymentIncomingRecord) => Promise<TransitionResult>, + ): Promise<void> { + return this.transitionExtra( + { + extraStores: [], + }, + f, + ); + } + + async transitionExtra< + StoreNameArray extends Array<StoreNames<typeof WalletStoresV1>> = [], + >( + opts: { extraStores: StoreNameArray }, + f: ( + rec: PeerPullPaymentIncomingRecord, + tx: DbReadWriteTransactionArr< + typeof WalletStoresV1, + ["peerPullDebit", ...StoreNameArray] + >, + ) => Promise<TransitionResult>, + ): Promise<void> { + const ws = this.ws; + const extraStores = opts.extraStores ?? []; + const transitionInfo = await ws.db.runReadWriteTx( + ["peerPullDebit", ...extraStores], + async (tx) => { + const pi = await tx.peerPullDebit.get(this.peerPullDebitId); + if (!pi) { + throw Error("peer pull payment not found anymore"); + } + const oldTxState = computePeerPullDebitTransactionState(pi); + const res = await f(pi, tx); + switch (res) { + case TransitionResult.Transition: { + await tx.peerPullDebit.put(pi); + const newTxState = computePeerPullDebitTransactionState(pi); + return { + oldTxState, + newTxState, + }; + } + default: + return undefined; + } + }, + ); + notifyTransition(ws, this.transactionId, transitionInfo); + } +} + async function handlePurseCreationConflict( - ws: InternalWalletState, + ctx: PeerPullDebitTransactionContext, peerPullInc: PeerPullPaymentIncomingRecord, resp: HttpResponse, ): Promise<TaskRunResult> { - const pursePub = peerPullInc.pursePub; + const ws = ctx.ws; const errResp = await readTalerErrorResponse(resp); if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { - await failPeerPullDebitTransaction(ws, pursePub); + await ctx.failTransaction(); return TaskRunResult.finished(); } @@ -139,29 +316,27 @@ async function handlePurseCreationConflict( coinSelRes.result.coins, ); - await ws.db - .mktx((x) => [x.peerPullDebit]) - .runReadWrite(async (tx) => { - const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId); - if (!myPpi) { - return; - } - switch (myPpi.status) { - case PeerPullDebitRecordStatus.PendingDeposit: - case PeerPullDebitRecordStatus.SuspendedDeposit: { - const sel = coinSelRes.result; - myPpi.coinSel = { - coinPubs: sel.coins.map((x) => x.coinPub), - contributions: sel.coins.map((x) => x.contribution), - totalCost: Amounts.stringify(totalAmount), - }; - break; - } - default: - return; + await ws.db.runReadWriteTx(["peerPullDebit"], async (tx) => { + const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId); + if (!myPpi) { + return; + } + switch (myPpi.status) { + case PeerPullDebitRecordStatus.PendingDeposit: + case PeerPullDebitRecordStatus.SuspendedDeposit: { + const sel = coinSelRes.result; + myPpi.coinSel = { + coinPubs: sel.coins.map((x) => x.coinPub), + contributions: sel.coins.map((x) => x.contribution), + totalCost: Amounts.stringify(totalAmount), + }; + break; } - await tx.peerPullDebit.put(myPpi); - }); + default: + return; + } + await tx.peerPullDebit.put(myPpi); + }); return TaskRunResult.finished(); } @@ -169,7 +344,6 @@ async function processPeerPullDebitPendingDeposit( ws: InternalWalletState, peerPullInc: PeerPullPaymentIncomingRecord, ): Promise<TaskRunResult> { - const peerPullDebitId = peerPullInc.peerPullDebitId; const pursePub = peerPullInc.pursePub; const coinSel = peerPullInc.coinSel; @@ -198,15 +372,16 @@ async function processPeerPullDebitPendingDeposit( logger.trace(`purse deposit payload: ${j2s(depositPayload)}`); } - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }); - const httpResp = await ws.http.fetch(purseDepositUrl.href, { method: "POST", body: depositPayload, }); + + const ctx = new PeerPullDebitTransactionContext( + ws, + peerPullInc.peerPullDebitId, + ); + switch (httpResp.status) { case HttpStatusCode.Ok: { const resp = await readSuccessResponseJsonOrThrow( @@ -215,77 +390,21 @@ async function processPeerPullDebitPendingDeposit( ); logger.trace(`purse deposit response: ${j2s(resp)}`); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullDebit]) - .runReadWrite(async (tx) => { - const pi = await tx.peerPullDebit.get(peerPullDebitId); - if (!pi) { - throw Error("peer pull payment not found anymore"); - } - if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { - return; - } - const oldTxState = computePeerPullDebitTransactionState(pi); - pi.status = PeerPullDebitRecordStatus.Done; - const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullDebit.put(pi); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - break; + await ctx.transition(async (r) => { + if (r.status !== PeerPullDebitRecordStatus.PendingDeposit) { + return TransitionResult.Stay; + } + r.status = PeerPullDebitRecordStatus.Done; + return TransitionResult.Transition; + }); + return TaskRunResult.finished(); } case HttpStatusCode.Gone: { - const transitionInfo = await ws.db - .mktx((x) => [ - x.peerPullDebit, - x.refreshGroups, - x.denominations, - x.coinAvailability, - x.coins, - ]) - .runReadWrite(async (tx) => { - const pi = await tx.peerPullDebit.get(peerPullDebitId); - if (!pi) { - throw Error("peer pull payment not found anymore"); - } - if (pi.status !== PeerPullDebitRecordStatus.PendingDeposit) { - return; - } - const oldTxState = computePeerPullDebitTransactionState(pi); - - const currency = Amounts.currencyOf(pi.totalCostEstimated); - const coinPubs: CoinRefreshRequest[] = []; - - if (!pi.coinSel) { - throw Error("invalid db state"); - } - - for (let i = 0; i < pi.coinSel.coinPubs.length; i++) { - coinPubs.push({ - amount: pi.coinSel.contributions[i], - coinPub: pi.coinSel.coinPubs[i], - }); - } - - const refresh = await createRefreshGroup( - ws, - tx, - currency, - coinPubs, - RefreshReason.AbortPeerPullDebit, - ); - - pi.status = PeerPullDebitRecordStatus.AbortingRefresh; - pi.abortRefreshGroupId = refresh.refreshGroupId; - const newTxState = computePeerPullDebitTransactionState(pi); - await tx.peerPullDebit.put(pi); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - break; + await ctx.abortTransaction(); + return TaskRunResult.finished(); } case HttpStatusCode.Conflict: { - return handlePurseCreationConflict(ws, peerPullInc, httpResp); + return handlePurseCreationConflict(ctx, peerPullInc, httpResp); } default: { const errResp = await readTalerErrorResponse(httpResp); @@ -295,7 +414,6 @@ async function processPeerPullDebitPendingDeposit( }; } } - return TaskRunResult.finished(); } async function processPeerPullDebitAbortingRefresh( @@ -624,6 +742,9 @@ export async function preparePeerPullDebit( }; } +/** + * FIXME: This belongs in the transaction context! + */ export async function suspendPeerPullDebitTransaction( ws: InternalWalletState, peerPullDebitId: string, @@ -683,182 +804,6 @@ export async function suspendPeerPullDebitTransaction( notifyTransition(ws, transactionId, transitionInfo); } -export async function abortPeerPullDebitTransaction( - ws: InternalWalletState, - peerPullDebitId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullDebitId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullDebit]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullDebitId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - newStatus = PeerPullDebitRecordStatus.Aborted; - break; - case PeerPullDebitRecordStatus.Done: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - newStatus = PeerPullDebitRecordStatus.AbortingRefresh; - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullDebit.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPullDebitTransaction( - ws: InternalWalletState, - peerPullDebitId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullDebitId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullDebit]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullDebitId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - newStatus = PeerPullDebitRecordStatus.Aborted; - break; - case PeerPullDebitRecordStatus.Done: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - case PeerPullDebitRecordStatus.AbortingRefresh: - // FIXME: abort underlying refresh! - newStatus = PeerPullDebitRecordStatus.Failed; - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullDebit.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPullDebitTransaction( - ws: InternalWalletState, - peerPullDebitId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullDebit, - peerPullDebitId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullDebit, - peerPullDebitId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullDebit]) - .runReadWrite(async (tx) => { - const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId); - if (!pullDebitRec) { - logger.warn(`peer pull debit ${peerPullDebitId} not found`); - return; - } - let newStatus: PeerPullDebitRecordStatus | undefined = undefined; - switch (pullDebitRec.status) { - case PeerPullDebitRecordStatus.DialogProposed: - case PeerPullDebitRecordStatus.Done: - case PeerPullDebitRecordStatus.PendingDeposit: - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - newStatus = PeerPullDebitRecordStatus.PendingDeposit; - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - break; - case PeerPullDebitRecordStatus.Failed: - break; - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - newStatus = PeerPullDebitRecordStatus.AbortingRefresh; - break; - default: - assertUnreachable(pullDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); - pullDebitRec.status = newStatus; - const newTxState = computePeerPullDebitTransactionState(pullDebitRec); - await tx.peerPullDebit.put(pullDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - export function computePeerPullDebitTransactionState( pullDebitRecord: PeerPullPaymentIncomingRecord, ): TransactionState { |