From 8da08fe4205c1e03eec3d4925c598be0b6769ba5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 15 Jan 2024 17:36:50 +0100 Subject: wallet-core: uniform transaction interface, cleanup --- packages/taler-wallet-core/src/db.ts | 1 + .../taler-wallet-core/src/internal-wallet-state.ts | 11 - .../taler-wallet-core/src/operations/common.ts | 10 +- .../taler-wallet-core/src/operations/deposits.ts | 404 ++++++++-------- .../src/operations/pay-merchant.ts | 422 +++++++++-------- .../src/operations/pay-peer-pull-credit.ts | 523 +++++++++++---------- .../src/operations/pay-peer-pull-debit.ts | 129 ++--- .../src/operations/pay-peer-push-credit.ts | 458 ++++++++++-------- .../src/operations/pay-peer-push-debit.ts | 522 ++++++++++---------- .../taler-wallet-core/src/operations/refresh.ts | 287 +++++------ .../taler-wallet-core/src/operations/reward.ts | 387 ++++++++------- .../src/operations/transactions.ts | 416 ++-------------- .../taler-wallet-core/src/operations/withdraw.ts | 482 ++++++++++--------- packages/taler-wallet-core/src/util/query.ts | 6 +- packages/taler-wallet-core/src/wallet.ts | 8 +- 15 files changed, 1939 insertions(+), 2127 deletions(-) (limited to 'packages/taler-wallet-core') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 263de9d4c..84066aaf0 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -957,6 +957,7 @@ export enum RewardRecordStatus { DialogAccept = 0x0101_0000, Done = 0x0500_0000, Aborted = 0x0500_0000, + Failed = 0x0501_000, } export enum RefreshCoinStatus { diff --git a/packages/taler-wallet-core/src/internal-wallet-state.ts b/packages/taler-wallet-core/src/internal-wallet-state.ts index 94f2367e1..8c49f8e5e 100644 --- a/packages/taler-wallet-core/src/internal-wallet-state.ts +++ b/packages/taler-wallet-core/src/internal-wallet-state.ts @@ -69,16 +69,6 @@ export interface MerchantInfo { protocolVersionCurrent: number; } -/** - * Interface for merchant-related operations. - */ -export interface MerchantOperations { - getMerchantInfo( - ws: InternalWalletState, - merchantBaseUrl: string, - ): Promise; -} - export interface RefreshOperations { createRefreshGroup( ws: InternalWalletState, @@ -154,7 +144,6 @@ export interface InternalWalletState { merchantInfoCache: Record; recoupOps: RecoupOperations; - merchantOps: MerchantOperations; refreshOps: RefreshOperations; isTaskLoopRunning: boolean; diff --git a/packages/taler-wallet-core/src/operations/common.ts b/packages/taler-wallet-core/src/operations/common.ts index 1103b7255..f34190cef 100644 --- a/packages/taler-wallet-core/src/operations/common.ts +++ b/packages/taler-wallet-core/src/operations/common.ts @@ -76,10 +76,7 @@ import { PendingTaskType, TaskId } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../util/query.js"; -import { - constructTransactionIdentifier, - parseTransactionIdentifier, -} from "./transactions.js"; +import { constructTransactionIdentifier } from "./transactions.js"; const logger = new Logger("operations/common.ts"); @@ -1086,11 +1083,12 @@ export enum TransitionResult { /** * Transaction context. - * - * FIXME: Should eventually be implemented by all transactions. + * Uniform interface to all transactions. */ export interface TransactionContext { abortTransaction(): Promise; + suspendTransaction(): Promise; resumeTransaction(): Promise; failTransaction(): Promise; + deleteTransaction(): Promise; } diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index f158d9cf9..62c1e406c 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -14,6 +14,10 @@ GNU Taler; see the file COPYING. If not, see */ +/** + * Implementation of the deposit transaction. + */ + /** * Imports. */ @@ -84,6 +88,7 @@ import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { TaskRunResult, TombstoneTag, + TransactionContext, constructTaskIdentifier, runLongpollAsync, spendCoins, @@ -106,6 +111,194 @@ import { */ const logger = new Logger("deposits.ts"); +export class DepositTransactionContext implements TransactionContext { + private transactionId: string; + private retryTag: string; + constructor( + public ws: InternalWalletState, + public depositGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Deposit, + depositGroupId, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Deposit, + depositGroupId, + }); + } + + async deleteTransaction(): Promise { + const depositGroupId = this.depositGroupId; + const ws = this.ws; + // FIXME: We should check first if we are in a final state + // where deletion is allowed. + await ws.db + .mktx((x) => [x.depositGroups, x.tombstones]) + .runReadWrite(async (tx) => { + const tipRecord = await tx.depositGroups.get(depositGroupId); + if (tipRecord) { + await tx.depositGroups.delete(depositGroupId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, + }); + } + }); + return; + } + + async suspendTransaction(): Promise { + const { ws, depositGroupId, transactionId, retryTag } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, + ); + return undefined; + } + const oldState = computeDepositTransactionStatus(dg); + let newOpStatus: DepositOperationStatus | undefined; + switch (dg.operationStatus) { + case DepositOperationStatus.PendingDeposit: + newOpStatus = DepositOperationStatus.SuspendedDeposit; + break; + case DepositOperationStatus.PendingKyc: + newOpStatus = DepositOperationStatus.SuspendedKyc; + break; + case DepositOperationStatus.PendingTrack: + newOpStatus = DepositOperationStatus.SuspendedTrack; + break; + case DepositOperationStatus.Aborting: + newOpStatus = DepositOperationStatus.SuspendedAborting; + break; + } + if (!newOpStatus) { + return undefined; + } + dg.operationStatus = newOpStatus; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + }); + stopLongpolling(ws, retryTag); + notifyTransition(ws, transactionId, transitionInfo); + } + + async abortTransaction(): Promise { + const { ws, depositGroupId, transactionId, retryTag } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, + ); + return undefined; + } + const oldState = computeDepositTransactionStatus(dg); + switch (dg.operationStatus) { + case DepositOperationStatus.Finished: + return undefined; + case DepositOperationStatus.PendingDeposit: { + dg.operationStatus = DepositOperationStatus.Aborting; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + } + case DepositOperationStatus.SuspendedDeposit: + // FIXME: Can we abort a suspended transaction?! + return undefined; + } + return undefined; + }); + stopLongpolling(ws, retryTag); + // Need to process the operation again. + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, depositGroupId, transactionId, retryTag } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't resume deposit group, depositGroupId=${depositGroupId} not found`, + ); + return; + } + const oldState = computeDepositTransactionStatus(dg); + let newOpStatus: DepositOperationStatus | undefined; + switch (dg.operationStatus) { + case DepositOperationStatus.SuspendedDeposit: + newOpStatus = DepositOperationStatus.PendingDeposit; + break; + case DepositOperationStatus.SuspendedAborting: + newOpStatus = DepositOperationStatus.Aborting; + break; + case DepositOperationStatus.SuspendedKyc: + newOpStatus = DepositOperationStatus.PendingKyc; + break; + case DepositOperationStatus.SuspendedTrack: + newOpStatus = DepositOperationStatus.PendingTrack; + break; + } + if (!newOpStatus) { + return undefined; + } + dg.operationStatus = newOpStatus; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, depositGroupId, transactionId, retryTag } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.depositGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.depositGroups.get(depositGroupId); + if (!dg) { + logger.warn( + `can't cancel aborting deposit group, depositGroupId=${depositGroupId} not found`, + ); + return undefined; + } + const oldState = computeDepositTransactionStatus(dg); + switch (dg.operationStatus) { + case DepositOperationStatus.SuspendedAborting: + case DepositOperationStatus.Aborting: { + dg.operationStatus = DepositOperationStatus.Failed; + await tx.depositGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeDepositTransactionStatus(dg), + }; + } + } + return undefined; + }); + // FIXME: Also cancel ongoing work (via cancellation token, once implemented) + stopLongpolling(ws, retryTag); + notifyTransition(ws, transactionId, transitionInfo); + } +} + /** * Get the (DD37-style) transaction status based on the * database record of a deposit group. @@ -203,217 +396,6 @@ export function computeDepositTransactionActions( } } -/** - * Put a deposit group in a suspended state. - * While the deposit group is suspended, no network requests - * will be made to advance the transaction status. - */ -export async function suspendDepositGroup( - ws: InternalWalletState, - depositGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.Deposit, - depositGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - logger.warn( - `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, - ); - return undefined; - } - const oldState = computeDepositTransactionStatus(dg); - let newOpStatus: DepositOperationStatus | undefined; - switch (dg.operationStatus) { - case DepositOperationStatus.PendingDeposit: - newOpStatus = DepositOperationStatus.SuspendedDeposit; - break; - case DepositOperationStatus.PendingKyc: - newOpStatus = DepositOperationStatus.SuspendedKyc; - break; - case DepositOperationStatus.PendingTrack: - newOpStatus = DepositOperationStatus.SuspendedTrack; - break; - case DepositOperationStatus.Aborting: - newOpStatus = DepositOperationStatus.SuspendedAborting; - break; - } - if (!newOpStatus) { - return undefined; - } - dg.operationStatus = newOpStatus; - await tx.depositGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeDepositTransactionStatus(dg), - }; - }); - stopLongpolling(ws, retryTag); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumeDepositGroup( - ws: InternalWalletState, - depositGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - logger.warn( - `can't resume deposit group, depositGroupId=${depositGroupId} not found`, - ); - return; - } - const oldState = computeDepositTransactionStatus(dg); - let newOpStatus: DepositOperationStatus | undefined; - switch (dg.operationStatus) { - case DepositOperationStatus.SuspendedDeposit: - newOpStatus = DepositOperationStatus.PendingDeposit; - break; - case DepositOperationStatus.SuspendedAborting: - newOpStatus = DepositOperationStatus.Aborting; - break; - case DepositOperationStatus.SuspendedKyc: - newOpStatus = DepositOperationStatus.PendingKyc; - break; - case DepositOperationStatus.SuspendedTrack: - newOpStatus = DepositOperationStatus.PendingTrack; - break; - } - if (!newOpStatus) { - return undefined; - } - dg.operationStatus = newOpStatus; - await tx.depositGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeDepositTransactionStatus(dg), - }; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortDepositGroup( - ws: InternalWalletState, - depositGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.Deposit, - depositGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - logger.warn( - `can't suspend deposit group, depositGroupId=${depositGroupId} not found`, - ); - return undefined; - } - const oldState = computeDepositTransactionStatus(dg); - switch (dg.operationStatus) { - case DepositOperationStatus.Finished: - return undefined; - case DepositOperationStatus.PendingDeposit: { - dg.operationStatus = DepositOperationStatus.Aborting; - await tx.depositGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeDepositTransactionStatus(dg), - }; - } - case DepositOperationStatus.SuspendedDeposit: - // FIXME: Can we abort a suspended transaction?! - return undefined; - } - return undefined; - }); - stopLongpolling(ws, retryTag); - // Need to process the operation again. - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failDepositTransaction( - ws: InternalWalletState, - depositGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Deposit, - depositGroupId, - }); - const retryTag = constructTaskIdentifier({ - tag: PendingTaskType.Deposit, - depositGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.depositGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.depositGroups.get(depositGroupId); - if (!dg) { - logger.warn( - `can't cancel aborting deposit group, depositGroupId=${depositGroupId} not found`, - ); - return undefined; - } - const oldState = computeDepositTransactionStatus(dg); - switch (dg.operationStatus) { - case DepositOperationStatus.SuspendedAborting: - case DepositOperationStatus.Aborting: { - dg.operationStatus = DepositOperationStatus.Failed; - await tx.depositGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeDepositTransactionStatus(dg), - }; - } - } - return undefined; - }); - // FIXME: Also cancel ongoing work (via cancellation token, once implemented) - stopLongpolling(ws, retryTag); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function deleteDepositGroup( - ws: InternalWalletState, - depositGroupId: string, -) { - // FIXME: We should check first if we are in a final state - // where deletion is allowed. - await ws.db - .mktx((x) => [x.depositGroups, x.tombstones]) - .runReadWrite(async (tx) => { - const tipRecord = await tx.depositGroups.get(depositGroupId); - if (tipRecord) { - await tx.depositGroups.delete(depositGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, - }); - } - }); -} - /** * Check whether the refresh associated with the * aborting deposit group is done. diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index a81311702..bc9e94a21 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -94,7 +94,6 @@ import { } from "@gnu-taler/taler-util/http"; import { EddsaKeypair } from "../crypto/cryptoImplementation.js"; import { - BackupProviderStateTag, CoinRecord, DenominationRecord, PurchaseRecord, @@ -130,6 +129,8 @@ import { TaskIdentifiers, TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, } from "./common.js"; import { calculateRefreshOutput, @@ -147,6 +148,224 @@ import { */ const logger = new Logger("pay-merchant.ts"); +export class PayMerchantTransactionContext implements TransactionContext { + private transactionId: string; + private retryTag: string; + + constructor( + public ws: InternalWalletState, + public proposalId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Purchase, + proposalId, + }); + } + + async deleteTransaction(): Promise { + const { ws, proposalId } = this; + await ws.db + .mktx((x) => [x.purchases, x.tombstones]) + .runReadWrite(async (tx) => { + let found = false; + const purchase = await tx.purchases.get(proposalId); + if (purchase) { + found = true; + await tx.purchases.delete(proposalId); + } + if (found) { + await tx.tombstones.put({ + id: TombstoneTag.DeletePayment + ":" + proposalId, + }); + } + }); + } + + async suspendTransaction(): Promise { + const { ws, proposalId, transactionId } = this; + stopLongpolling(ws, this.retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.purchases]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) { + throw Error("purchase not found"); + } + const oldTxState = computePayMerchantTransactionState(purchase); + let newStatus = transitionSuspend[purchase.purchaseStatus]; + if (!newStatus) { + return undefined; + } + await tx.purchases.put(purchase); + const newTxState = computePayMerchantTransactionState(purchase); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + ws.workAvailable.trigger(); + } + + async abortTransaction(): Promise { + const { ws, proposalId, transactionId } = this; + const transitionInfo = await ws.db + .mktx((x) => [ + x.purchases, + x.refreshGroups, + x.denominations, + x.coinAvailability, + x.coins, + x.operationRetries, + ]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) { + throw Error("purchase not found"); + } + const oldTxState = computePayMerchantTransactionState(purchase); + const oldStatus = purchase.purchaseStatus; + if (purchase.timestampFirstSuccessfulPay) { + // No point in aborting it. We don't even report an error. + logger.warn(`tried to abort successful payment`); + return; + } + if (oldStatus === PurchaseStatus.PendingPaying) { + purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund; + } + await tx.purchases.put(purchase); + if (oldStatus === PurchaseStatus.PendingPaying) { + if (purchase.payInfo) { + const coinSel = purchase.payInfo.payCoinSelection; + const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost); + const refreshCoins: CoinRefreshRequest[] = []; + for (let i = 0; i < coinSel.coinPubs.length; i++) { + refreshCoins.push({ + amount: coinSel.coinContributions[i], + coinPub: coinSel.coinPubs[i], + }); + } + await createRefreshGroup( + ws, + tx, + currency, + refreshCoins, + RefreshReason.AbortPay, + ); + } + } + await tx.operationRetries.delete(this.retryTag); + const newTxState = computePayMerchantTransactionState(purchase); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + ws.workAvailable.trigger(); + } + + async resumeTransaction(): Promise { + const { ws, proposalId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.purchases]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) { + throw Error("purchase not found"); + } + const oldTxState = computePayMerchantTransactionState(purchase); + let newStatus = transitionResume[purchase.purchaseStatus]; + if (!newStatus) { + return undefined; + } + await tx.purchases.put(purchase); + const newTxState = computePayMerchantTransactionState(purchase); + return { oldTxState, newTxState }; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + ws.workAvailable.trigger(); + } + + async failTransaction(): Promise { + const { ws, proposalId, transactionId } = this; + const transitionInfo = await ws.db + .mktx((x) => [ + x.purchases, + x.refreshGroups, + x.denominations, + x.coinAvailability, + x.coins, + x.operationRetries, + ]) + .runReadWrite(async (tx) => { + const purchase = await tx.purchases.get(proposalId); + if (!purchase) { + throw Error("purchase not found"); + } + const oldTxState = computePayMerchantTransactionState(purchase); + let newState: PurchaseStatus | undefined = undefined; + switch (purchase.purchaseStatus) { + case PurchaseStatus.AbortingWithRefund: + newState = PurchaseStatus.FailedAbort; + break; + } + if (newState) { + purchase.purchaseStatus = newState; + await tx.purchases.put(purchase); + } + const newTxState = computePayMerchantTransactionState(purchase); + return { oldTxState, newTxState }; + }); + notifyTransition(ws, transactionId, transitionInfo); + ws.workAvailable.trigger(); + } +} + +export class RefundTransactionContext implements TransactionContext { + public transactionId: string; + constructor( + public ws: InternalWalletState, + public refundGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Refund, + refundGroupId, + }); + } + + async deleteTransaction(): Promise { + const { ws, refundGroupId, transactionId } = this; + await ws.db + .mktx((x) => [x.refundGroups, x.tombstones]) + .runReadWrite(async (tx) => { + const refundRecord = await tx.refundGroups.get(refundGroupId); + if (!refundRecord) { + return; + } + await tx.refundGroups.delete(refundGroupId); + await tx.tombstones.put({ id: transactionId }); + // FIXME: Also tombstone the refund items, so that they won't reappear. + }); + } + + suspendTransaction(): Promise { + throw new Error("Unsupported operation"); + } + + abortTransaction(): Promise { + throw new Error("Unsupported operation"); + } + + resumeTransaction(): Promise { + throw new Error("Unsupported operation"); + } + + failTransaction(): Promise { + throw new Error("Unsupported operation"); + } +} + /** * Compute the total cost of a payment to the customer. * @@ -949,27 +1168,6 @@ async function handleInsufficientFunds( }); } -async function unblockBackup( - ws: InternalWalletState, - proposalId: string, -): Promise { - await ws.db - .mktx((x) => [x.backupProviders]) - .runReadWrite(async (tx) => { - await tx.backupProviders.indexes.byPaymentProposalId - .iter(proposalId) - .forEachAsync(async (bp) => { - bp.state = { - tag: BackupProviderStateTag.Ready, - nextBackupTimestamp: timestampPreciseToDb( - TalerPreciseTimestamp.now(), - ), - }; - tx.backupProviders.put(bp); - }); - }); -} - // FIXME: Should probably not be exported in its current state // FIXME: Should take a transaction ID instead of a proposal ID // FIXME: Does way more than checking the payment @@ -1606,7 +1804,7 @@ export async function processPurchase( } } -export async function processPurchasePay( +async function processPurchasePay( ws: InternalWalletState, proposalId: string, options: unknown = {}, @@ -1772,7 +1970,6 @@ export async function processPurchasePay( } await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp); - await unblockBackup(ws, proposalId); } else { const payAgainUrl = new URL( `orders/${download.contractData.orderId}/paid`, @@ -1799,7 +1996,6 @@ export async function processPurchasePay( ); } await storePayReplaySuccess(ws, proposalId, sessionId); - await unblockBackup(ws, proposalId); } return TaskRunResult.finished(); @@ -1837,115 +2033,6 @@ export async function refuseProposal( notifyTransition(ws, transactionId, transitionInfo); } -export async function abortPayMerchant( - ws: InternalWalletState, - proposalId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - const opId = constructTaskIdentifier({ - tag: PendingTaskType.Purchase, - proposalId, - }); - const transitionInfo = await ws.db - .mktx((x) => [ - x.purchases, - x.refreshGroups, - x.denominations, - x.coinAvailability, - x.coins, - x.operationRetries, - ]) - .runReadWrite(async (tx) => { - const purchase = await tx.purchases.get(proposalId); - if (!purchase) { - throw Error("purchase not found"); - } - const oldTxState = computePayMerchantTransactionState(purchase); - const oldStatus = purchase.purchaseStatus; - if (purchase.timestampFirstSuccessfulPay) { - // No point in aborting it. We don't even report an error. - logger.warn(`tried to abort successful payment`); - return; - } - if (oldStatus === PurchaseStatus.PendingPaying) { - purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund; - } - await tx.purchases.put(purchase); - if (oldStatus === PurchaseStatus.PendingPaying) { - if (purchase.payInfo) { - const coinSel = purchase.payInfo.payCoinSelection; - const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost); - const refreshCoins: CoinRefreshRequest[] = []; - for (let i = 0; i < coinSel.coinPubs.length; i++) { - refreshCoins.push({ - amount: coinSel.coinContributions[i], - coinPub: coinSel.coinPubs[i], - }); - } - await createRefreshGroup( - ws, - tx, - currency, - refreshCoins, - RefreshReason.AbortPay, - ); - } - } - await tx.operationRetries.delete(opId); - const newTxState = computePayMerchantTransactionState(purchase); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - ws.workAvailable.trigger(); -} - -export async function failPaymentTransaction( - ws: InternalWalletState, - proposalId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - const opId = constructTaskIdentifier({ - tag: PendingTaskType.Purchase, - proposalId, - }); - const transitionInfo = await ws.db - .mktx((x) => [ - x.purchases, - x.refreshGroups, - x.denominations, - x.coinAvailability, - x.coins, - x.operationRetries, - ]) - .runReadWrite(async (tx) => { - const purchase = await tx.purchases.get(proposalId); - if (!purchase) { - throw Error("purchase not found"); - } - const oldTxState = computePayMerchantTransactionState(purchase); - let newState: PurchaseStatus | undefined = undefined; - switch (purchase.purchaseStatus) { - case PurchaseStatus.AbortingWithRefund: - newState = PurchaseStatus.FailedAbort; - break; - } - if (newState) { - purchase.purchaseStatus = newState; - await tx.purchases.put(purchase); - } - const newTxState = computePayMerchantTransactionState(purchase); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - ws.workAvailable.trigger(); -} - const transitionSuspend: { [x in PurchaseStatus]?: { next: PurchaseStatus | undefined; @@ -1990,73 +2077,6 @@ const transitionResume: { }, }; -export async function suspendPayMerchant( - ws: InternalWalletState, - proposalId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - const opId = constructTaskIdentifier({ - tag: PendingTaskType.Purchase, - proposalId, - }); - stopLongpolling(ws, opId); - const transitionInfo = await ws.db - .mktx((x) => [x.purchases]) - .runReadWrite(async (tx) => { - const purchase = await tx.purchases.get(proposalId); - if (!purchase) { - throw Error("purchase not found"); - } - const oldTxState = computePayMerchantTransactionState(purchase); - let newStatus = transitionSuspend[purchase.purchaseStatus]; - if (!newStatus) { - return undefined; - } - await tx.purchases.put(purchase); - const newTxState = computePayMerchantTransactionState(purchase); - return { oldTxState, newTxState }; - }); - notifyTransition(ws, transactionId, transitionInfo); - ws.workAvailable.trigger(); -} - -export async function resumePayMerchant( - ws: InternalWalletState, - proposalId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId, - }); - const opId = constructTaskIdentifier({ - tag: PendingTaskType.Purchase, - proposalId, - }); - stopLongpolling(ws, opId); - const transitionInfo = await ws.db - .mktx((x) => [x.purchases]) - .runReadWrite(async (tx) => { - const purchase = await tx.purchases.get(proposalId); - if (!purchase) { - throw Error("purchase not found"); - } - const oldTxState = computePayMerchantTransactionState(purchase); - let newStatus = transitionResume[purchase.purchaseStatus]; - if (!newStatus) { - return undefined; - } - await tx.purchases.put(purchase); - const newTxState = computePayMerchantTransactionState(purchase); - return { oldTxState, newTxState }; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); - ws.workAvailable.trigger(); -} - export function computePayMerchantTransactionState( purchaseRecord: PurchaseRecord, ): TransactionState { diff --git a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts index a90eceed7..e655eba4b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-pull-credit.ts @@ -69,6 +69,8 @@ import { LongpollResult, TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, constructTaskIdentifier, runLongpollAsync, } from "./common.js"; @@ -88,6 +90,275 @@ import { const logger = new Logger("pay-peer-pull-credit.ts"); +export class PeerPullCreditTransactionContext implements TransactionContext { + private transactionId: string; + private retryTag: string; + + constructor( + public ws: InternalWalletState, + public pursePub: string, + ) { + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullCredit, + pursePub, + }); + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullCredit, + pursePub, + }); + } + + async deleteTransaction(): Promise { + const { ws, pursePub } = this; + await ws.db + .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones]) + .runReadWrite(async (tx) => { + const pullIni = await tx.peerPullCredit.get(pursePub); + if (!pullIni) { + return; + } + if (pullIni.withdrawalGroupId) { + const withdrawalGroupId = pullIni.withdrawalGroupId; + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); + if (withdrawalGroupRecord) { + await tx.withdrawalGroups.delete(withdrawalGroupId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, + }); + } + } + await tx.peerPullCredit.delete(pursePub); + await tx.tombstones.put({ + id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, + }); + }); + + return; + } + + async suspendTransaction(): Promise { + const { ws, pursePub, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullCredit]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullCredit.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentCreditStatus.PendingCreatePurse: + newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse; + break; + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired; + break; + case PeerPullPaymentCreditStatus.PendingWithdrawing: + newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing; + break; + case PeerPullPaymentCreditStatus.PendingReady: + newStatus = PeerPullPaymentCreditStatus.SuspendedReady; + break; + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + newStatus = + PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse; + break; + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.Expired: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = + computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, pursePub, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullCredit]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullCredit.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingReady: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.Expired: + break; + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentCreditStatus.Failed; + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = + computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, pursePub, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullCredit]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullCredit.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingWithdrawing: + case PeerPullPaymentCreditStatus.PendingReady: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.Expired: + case PeerPullPaymentCreditStatus.Aborted: + break; + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse; + break; + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired; + break; + case PeerPullPaymentCreditStatus.SuspendedReady: + newStatus = PeerPullPaymentCreditStatus.PendingReady; + break; + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing; + break; + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = + computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async abortTransaction(): Promise { + const { ws, pursePub, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullCredit]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullCredit.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; + break; + case PeerPullPaymentCreditStatus.PendingWithdrawing: + throw Error("can't abort anymore"); + case PeerPullPaymentCreditStatus.PendingReady: + newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; + break; + case PeerPullPaymentCreditStatus.Done: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedWithdrawing: + case PeerPullPaymentCreditStatus.Aborted: + case PeerPullPaymentCreditStatus.AbortingDeletePurse: + case PeerPullPaymentCreditStatus.Failed: + case PeerPullPaymentCreditStatus.Expired: + case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = + computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullCredit.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } +} + async function queryPurseForPeerPullCredit( ws: InternalWalletState, pullIni: PeerPullCreditRecord, @@ -849,258 +1120,6 @@ export async function initiatePeerPullPayment( }; } -export async function suspendPeerPullCreditTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullCredit]) - .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse; - break; - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired; - break; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing; - break; - case PeerPullPaymentCreditStatus.PendingReady: - newStatus = PeerPullPaymentCreditStatus.SuspendedReady; - break; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - newStatus = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPullCreditTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullCredit]) - .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.PendingWithdrawing: - throw Error("can't abort anymore"); - case PeerPullPaymentCreditStatus.PendingReady: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPullCreditTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullCredit]) - .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingWithdrawing: - case PeerPullPaymentCreditStatus.PendingReady: - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - case PeerPullPaymentCreditStatus.Aborted: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - break; - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentCreditStatus.Failed; - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPullCreditTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPullCredit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPullCredit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPullCredit]) - .runReadWrite(async (tx) => { - const pullCreditRec = await tx.peerPullCredit.get(pursePub); - if (!pullCreditRec) { - logger.warn(`peer pull credit ${pursePub} not found`); - return; - } - let newStatus: PeerPullPaymentCreditStatus | undefined = undefined; - switch (pullCreditRec.status) { - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingWithdrawing: - case PeerPullPaymentCreditStatus.PendingReady: - case PeerPullPaymentCreditStatus.AbortingDeletePurse: - case PeerPullPaymentCreditStatus.Done: - case PeerPullPaymentCreditStatus.Failed: - case PeerPullPaymentCreditStatus.Expired: - case PeerPullPaymentCreditStatus.Aborted: - break; - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: - newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse; - break; - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired; - break; - case PeerPullPaymentCreditStatus.SuspendedReady: - newStatus = PeerPullPaymentCreditStatus.PendingReady; - break; - case PeerPullPaymentCreditStatus.SuspendedWithdrawing: - newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing; - break; - case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse; - break; - default: - assertUnreachable(pullCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); - pullCreditRec.status = newStatus; - const newTxState = computePeerPullCreditTransactionState(pullCreditRec); - await tx.peerPullCredit.put(pullCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - export function computePeerPullCreditTransactionState( pullCreditRecord: PeerPullCreditRecord, ): TransactionState { 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 9bbe2c875..0f9f29fb5 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 @@ -120,6 +120,73 @@ export class PeerPullDebitTransactionContext implements TransactionContext { this.peerPullDebitId = peerPullDebitId; } + async deleteTransaction(): Promise { + const transactionId = this.transactionId; + const ws = this.ws; + const peerPullDebitId = this.peerPullDebitId; + await ws.db + .mktx((x) => [x.peerPullDebit, x.tombstones]) + .runReadWrite(async (tx) => { + const debit = await tx.peerPullDebit.get(peerPullDebitId); + if (debit) { + await tx.peerPullDebit.delete(peerPullDebitId); + await tx.tombstones.put({ id: transactionId }); + } + }); + } + + async suspendTransaction(): Promise { + const taskId = this.taskId; + const transactionId = this.transactionId; + const ws = this.ws; + const peerPullDebitId = this.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: + break; + case PeerPullDebitRecordStatus.Done: + break; + case PeerPullDebitRecordStatus.PendingDeposit: + newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; + break; + case PeerPullDebitRecordStatus.SuspendedDeposit: + break; + case PeerPullDebitRecordStatus.Aborted: + break; + case PeerPullDebitRecordStatus.AbortingRefresh: + newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; + 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); + } + async resumeTransaction(): Promise { const ctx = this; stopLongpolling(ctx.ws, ctx.taskId); @@ -742,68 +809,6 @@ export async function preparePeerPullDebit( }; } -/** - * FIXME: This belongs in the transaction context! - */ -export async function suspendPeerPullDebitTransaction( - 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: - break; - case PeerPullDebitRecordStatus.Done: - break; - case PeerPullDebitRecordStatus.PendingDeposit: - newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; - break; - case PeerPullDebitRecordStatus.SuspendedDeposit: - break; - case PeerPullDebitRecordStatus.Aborted: - break; - case PeerPullDebitRecordStatus.AbortingRefresh: - newStatus = PeerPullDebitRecordStatus.SuspendedAbortingRefresh; - 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 function computePeerPullDebitTransactionState( pullDebitRecord: PeerPullPaymentIncomingRecord, ): TransactionState { diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts index 36606e732..c8cfaac7d 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-credit.ts @@ -66,6 +66,8 @@ import { checkDbInvariant } from "../util/invariants.js"; import { TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, constructTaskIdentifier, runLongpollAsync, } from "./common.js"; @@ -90,6 +92,268 @@ import { const logger = new Logger("pay-peer-push-credit.ts"); +export class PeerPushCreditTransactionContext implements TransactionContext { + private transactionId: string; + private retryTag: string; + + constructor( + public ws: InternalWalletState, + public peerPushCreditId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushCreditId, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushCreditId, + }); + } + + async deleteTransaction(): Promise { + const { ws, peerPushCreditId } = this; + await ws.db.runReadWriteTx( + ["withdrawalGroups", "peerPushCredit", "tombstones"], + async (tx) => { + const pushInc = await tx.peerPushCredit.get(peerPushCreditId); + if (!pushInc) { + return; + } + if (pushInc.withdrawalGroupId) { + const withdrawalGroupId = pushInc.withdrawalGroupId; + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); + if (withdrawalGroupRecord) { + await tx.withdrawalGroups.delete(withdrawalGroupId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, + }); + } + } + await tx.peerPushCredit.delete(peerPushCreditId); + await tx.tombstones.put({ + id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId, + }); + }, + ); + return; + } + + async suspendTransaction(): Promise { + const { ws, peerPushCreditId, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db.runReadWriteTx( + ["peerPushCredit"], + async (tx) => { + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushCreditId} not found`); + return; + } + let newStatus: PeerPushCreditStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: + break; + case PeerPushCreditStatus.PendingMergeKycRequired: + newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired; + break; + case PeerPushCreditStatus.PendingMerge: + newStatus = PeerPushCreditStatus.SuspendedMerge; + break; + case PeerPushCreditStatus.PendingWithdrawing: + // FIXME: Suspend internal withdrawal transaction! + newStatus = PeerPushCreditStatus.SuspendedWithdrawing; + break; + case PeerPushCreditStatus.Aborted: + break; + case PeerPushCreditStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = + computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }, + ); + notifyTransition(ws, transactionId, transitionInfo); + } + + async abortTransaction(): Promise { + const { ws, peerPushCreditId, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db.runReadWriteTx( + ["peerPushCredit"], + async (tx) => { + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushCreditId} not found`); + return; + } + let newStatus: PeerPushCreditStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushCreditStatus.DialogProposed: + newStatus = PeerPushCreditStatus.Aborted; + break; + case PeerPushCreditStatus.Done: + break; + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: + newStatus = PeerPushCreditStatus.Aborted; + break; + case PeerPushCreditStatus.PendingMergeKycRequired: + newStatus = PeerPushCreditStatus.Aborted; + break; + case PeerPushCreditStatus.PendingMerge: + newStatus = PeerPushCreditStatus.Aborted; + break; + case PeerPushCreditStatus.PendingWithdrawing: + newStatus = PeerPushCreditStatus.Aborted; + break; + case PeerPushCreditStatus.Aborted: + break; + case PeerPushCreditStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = + computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }, + ); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, peerPushCreditId, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db.runReadWriteTx( + ["peerPushCredit"], + async (tx) => { + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushCreditId} not found`); + return; + } + let newStatus: PeerPushCreditStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingWithdrawing: + case PeerPushCreditStatus.SuspendedMerge: + newStatus = PeerPushCreditStatus.PendingMerge; + break; + case PeerPushCreditStatus.SuspendedMergeKycRequired: + newStatus = PeerPushCreditStatus.PendingMergeKycRequired; + break; + case PeerPushCreditStatus.SuspendedWithdrawing: + // FIXME: resume underlying "internal-withdrawal" transaction. + newStatus = PeerPushCreditStatus.PendingWithdrawing; + break; + case PeerPushCreditStatus.Aborted: + break; + case PeerPushCreditStatus.Failed: + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = + computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }, + ); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, peerPushCreditId, retryTag, transactionId } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db.runReadWriteTx( + ["peerPushCredit"], + async (tx) => { + const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushCreditId} not found`); + return; + } + let newStatus: PeerPushCreditStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushCreditStatus.Done: + case PeerPushCreditStatus.Aborted: + case PeerPushCreditStatus.Failed: + // Already in a final state. + return; + case PeerPushCreditStatus.DialogProposed: + case PeerPushCreditStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingWithdrawing: + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedMergeKycRequired: + case PeerPushCreditStatus.SuspendedWithdrawing: + newStatus = PeerPushCreditStatus.Failed; + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = + computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = + computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushCredit.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }, + ); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } +} + export async function preparePeerPushCredit( ws: InternalWalletState, req: PreparePeerPushCreditRequest, @@ -688,200 +952,6 @@ export async function confirmPeerPushCredit( }; } -export async function suspendPeerPushCreditTransaction( - ws: InternalWalletState, - peerPushCreditId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushCredit, - peerPushCreditId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushCredit]) - .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushCreditId} not found`); - return; - } - let newStatus: PeerPushCreditStatus | undefined = undefined; - switch (pushCreditRec.status) { - case PeerPushCreditStatus.DialogProposed: - case PeerPushCreditStatus.Done: - case PeerPushCreditStatus.SuspendedMerge: - case PeerPushCreditStatus.SuspendedMergeKycRequired: - case PeerPushCreditStatus.SuspendedWithdrawing: - break; - case PeerPushCreditStatus.PendingMergeKycRequired: - newStatus = PeerPushCreditStatus.SuspendedMergeKycRequired; - break; - case PeerPushCreditStatus.PendingMerge: - newStatus = PeerPushCreditStatus.SuspendedMerge; - break; - case PeerPushCreditStatus.PendingWithdrawing: - // FIXME: Suspend internal withdrawal transaction! - newStatus = PeerPushCreditStatus.SuspendedWithdrawing; - break; - case PeerPushCreditStatus.Aborted: - break; - case PeerPushCreditStatus.Failed: - break; - default: - assertUnreachable(pushCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); - pushCreditRec.status = newStatus; - const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushCredit.put(pushCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function abortPeerPushCreditTransaction( - ws: InternalWalletState, - peerPushCreditId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushCredit, - peerPushCreditId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushCredit]) - .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushCreditId} not found`); - return; - } - let newStatus: PeerPushCreditStatus | undefined = undefined; - switch (pushCreditRec.status) { - case PeerPushCreditStatus.DialogProposed: - newStatus = PeerPushCreditStatus.Aborted; - break; - case PeerPushCreditStatus.Done: - break; - case PeerPushCreditStatus.SuspendedMerge: - case PeerPushCreditStatus.SuspendedMergeKycRequired: - case PeerPushCreditStatus.SuspendedWithdrawing: - newStatus = PeerPushCreditStatus.Aborted; - break; - case PeerPushCreditStatus.PendingMergeKycRequired: - newStatus = PeerPushCreditStatus.Aborted; - break; - case PeerPushCreditStatus.PendingMerge: - newStatus = PeerPushCreditStatus.Aborted; - break; - case PeerPushCreditStatus.PendingWithdrawing: - newStatus = PeerPushCreditStatus.Aborted; - break; - case PeerPushCreditStatus.Aborted: - break; - case PeerPushCreditStatus.Failed: - break; - default: - assertUnreachable(pushCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); - pushCreditRec.status = newStatus; - const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushCredit.put(pushCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPushCreditTransaction( - ws: InternalWalletState, - peerPushCreditId: string, -) { - // We don't have any "aborting" states! - throw Error("can't run cancel-aborting on peer-push-credit transaction"); -} - -export async function resumePeerPushCreditTransaction( - ws: InternalWalletState, - peerPushCreditId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushCredit, - peerPushCreditId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushCredit, - peerPushCreditId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushCredit]) - .runReadWrite(async (tx) => { - const pushCreditRec = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushCreditRec) { - logger.warn(`peer push credit ${peerPushCreditId} not found`); - return; - } - let newStatus: PeerPushCreditStatus | undefined = undefined; - switch (pushCreditRec.status) { - case PeerPushCreditStatus.DialogProposed: - case PeerPushCreditStatus.Done: - case PeerPushCreditStatus.PendingMergeKycRequired: - case PeerPushCreditStatus.PendingMerge: - case PeerPushCreditStatus.PendingWithdrawing: - case PeerPushCreditStatus.SuspendedMerge: - newStatus = PeerPushCreditStatus.PendingMerge; - break; - case PeerPushCreditStatus.SuspendedMergeKycRequired: - newStatus = PeerPushCreditStatus.PendingMergeKycRequired; - break; - case PeerPushCreditStatus.SuspendedWithdrawing: - // FIXME: resume underlying "internal-withdrawal" transaction. - newStatus = PeerPushCreditStatus.PendingWithdrawing; - break; - case PeerPushCreditStatus.Aborted: - break; - case PeerPushCreditStatus.Failed: - break; - default: - assertUnreachable(pushCreditRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); - pushCreditRec.status = newStatus; - const newTxState = computePeerPushCreditTransactionState(pushCreditRec); - await tx.peerPushCredit.put(pushCreditRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - export function computePeerPushCreditTransactionState( pushCreditRecord: PeerPushPaymentIncomingRecord, ): TransactionState { diff --git a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts index a11ffe774..4fd1ef3b2 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer-push-debit.ts @@ -63,6 +63,7 @@ import { checkLogicInvariant } from "../util/invariants.js"; import { TaskRunResult, TaskRunResultType, + TransactionContext, constructTaskIdentifier, runLongpollAsync, spendCoins, @@ -80,6 +81,257 @@ import { const logger = new Logger("pay-peer-push-debit.ts"); +export class PeerPushDebitTransactionContext implements TransactionContext { + public transactionId: string; + public retryTag: string; + + constructor( + public ws: InternalWalletState, + public pursePub: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub, + }); + } + + async deleteTransaction(): Promise { + const { ws, pursePub, transactionId } = this; + await ws.db + .mktx((x) => [x.peerPushDebit, x.tombstones]) + .runReadWrite(async (tx) => { + const debit = await tx.peerPushDebit.get(pursePub); + if (debit) { + await tx.peerPushDebit.delete(pursePub); + await tx.tombstones.put({ id: transactionId }); + } + }); + } + + async suspendTransaction(): Promise { + const { ws, pursePub, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushDebit]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushDebit.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushDebitStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushDebitStatus.PendingCreatePurse: + newStatus = PeerPushDebitStatus.SuspendedCreatePurse; + break; + case PeerPushDebitStatus.AbortingRefreshDeleted: + newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted; + break; + case PeerPushDebitStatus.AbortingRefreshExpired: + newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired; + break; + case PeerPushDebitStatus.AbortingDeletePurse: + newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse; + break; + case PeerPushDebitStatus.PendingReady: + newStatus = PeerPushDebitStatus.SuspendedReady; + break; + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + case PeerPushDebitStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async abortTransaction(): Promise { + const { ws, pursePub, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushDebit]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushDebit.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushDebitStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.SuspendedReady: + newStatus = PeerPushDebitStatus.AbortingDeletePurse; + break; + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: + // Network request might already be in-flight! + newStatus = PeerPushDebitStatus.AbortingDeletePurse; + break; + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.AbortingRefreshExpired: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Expired: + case PeerPushDebitStatus.Failed: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, pursePub, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushDebit]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushDebit.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushDebitStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + newStatus = PeerPushDebitStatus.AbortingDeletePurse; + break; + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + newStatus = PeerPushDebitStatus.AbortingRefreshDeleted; + break; + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + newStatus = PeerPushDebitStatus.AbortingRefreshExpired; + break; + case PeerPushDebitStatus.SuspendedReady: + newStatus = PeerPushDebitStatus.PendingReady; + break; + case PeerPushDebitStatus.SuspendedCreatePurse: + newStatus = PeerPushDebitStatus.PendingCreatePurse; + break; + case PeerPushDebitStatus.PendingCreatePurse: + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.AbortingRefreshExpired: + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, pursePub, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushDebit]) + .runReadWrite(async (tx) => { + const pushDebitRec = await tx.peerPushDebit.get(pursePub); + if (!pushDebitRec) { + logger.warn(`peer push debit ${pursePub} not found`); + return; + } + let newStatus: PeerPushDebitStatus | undefined = undefined; + switch (pushDebitRec.status) { + case PeerPushDebitStatus.AbortingRefreshDeleted: + case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: + // FIXME: What to do about the refresh group? + newStatus = PeerPushDebitStatus.Failed; + break; + case PeerPushDebitStatus.AbortingDeletePurse: + case PeerPushDebitStatus.SuspendedAbortingDeletePurse: + case PeerPushDebitStatus.AbortingRefreshExpired: + case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: + case PeerPushDebitStatus.PendingReady: + case PeerPushDebitStatus.SuspendedReady: + case PeerPushDebitStatus.SuspendedCreatePurse: + case PeerPushDebitStatus.PendingCreatePurse: + newStatus = PeerPushDebitStatus.Failed; + break; + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Expired: + // Do nothing + break; + default: + assertUnreachable(pushDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); + pushDebitRec.status = newStatus; + const newTxState = computePeerPushDebitTransactionState(pushDebitRec); + await tx.peerPushDebit.put(pushDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } +} + export async function checkPeerPushDebit( ws: InternalWalletState, req: CheckPeerPushDebitRequest, @@ -118,8 +370,9 @@ async function handlePurseCreationConflict( ): Promise { const pursePub = peerPushInitiation.pursePub; const errResp = await readTalerErrorResponse(resp); + const ctx = new PeerPushDebitTransactionContext(ws, pursePub); if (errResp.code !== TalerErrorCode.EXCHANGE_GENERIC_INSUFFICIENT_FUNDS) { - await failPeerPushDebitTransaction(ws, pursePub); + await ctx.failTransaction(); return TaskRunResult.finished(); } @@ -189,10 +442,8 @@ async function processPeerPushDebitCreateReserve( const pursePub = peerPushInitiation.pursePub; const purseExpiration = peerPushInitiation.purseExpiration; const hContractTerms = peerPushInitiation.contractTermsHash; - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub: pursePub, - }); + const ctx = new PeerPushDebitTransactionContext(ws, pursePub); + const transactionId = ctx.transactionId; logger.trace(`processing ${transactionId} pending(create-reserve)`); @@ -277,7 +528,7 @@ async function processPeerPushDebitCreateReserve( break; case HttpStatusCode.Forbidden: { // FIXME: Store this error! - await failPeerPushDebitTransaction(ws, pursePub); + await ctx.failTransaction(); return TaskRunResult.finished(); } case HttpStatusCode.Conflict: { @@ -838,265 +1089,6 @@ export function computePeerPushDebitTransactionActions( } } -export async function abortPeerPushDebitTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushDebit]) - .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.SuspendedReady: - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.PendingCreatePurse: - // Network request might already be in-flight! - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Expired: - case PeerPushDebitStatus.Failed: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failPeerPushDebitTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushDebit]) - .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - // FIXME: What to do about the refresh group? - newStatus = PeerPushDebitStatus.Failed; - break; - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.SuspendedReady: - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.PendingCreatePurse: - newStatus = PeerPushDebitStatus.Failed; - break; - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function suspendPeerPushDebitTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushDebit]) - .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.PendingCreatePurse: - newStatus = PeerPushDebitStatus.SuspendedCreatePurse; - break; - case PeerPushDebitStatus.AbortingRefreshDeleted: - newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshDeleted; - break; - case PeerPushDebitStatus.AbortingRefreshExpired: - newStatus = PeerPushDebitStatus.SuspendedAbortingRefreshExpired; - break; - case PeerPushDebitStatus.AbortingDeletePurse: - newStatus = PeerPushDebitStatus.SuspendedAbortingDeletePurse; - break; - case PeerPushDebitStatus.PendingReady: - newStatus = PeerPushDebitStatus.SuspendedReady; - break; - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - case PeerPushDebitStatus.SuspendedReady: - case PeerPushDebitStatus.SuspendedCreatePurse: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumePeerPushDebitTransaction( - ws: InternalWalletState, - pursePub: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.PeerPushDebit, - pursePub, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.PeerPushDebit, - pursePub, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.peerPushDebit]) - .runReadWrite(async (tx) => { - const pushDebitRec = await tx.peerPushDebit.get(pursePub); - if (!pushDebitRec) { - logger.warn(`peer push debit ${pursePub} not found`); - return; - } - let newStatus: PeerPushDebitStatus | undefined = undefined; - switch (pushDebitRec.status) { - case PeerPushDebitStatus.SuspendedAbortingDeletePurse: - newStatus = PeerPushDebitStatus.AbortingDeletePurse; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshDeleted: - newStatus = PeerPushDebitStatus.AbortingRefreshDeleted; - break; - case PeerPushDebitStatus.SuspendedAbortingRefreshExpired: - newStatus = PeerPushDebitStatus.AbortingRefreshExpired; - break; - case PeerPushDebitStatus.SuspendedReady: - newStatus = PeerPushDebitStatus.PendingReady; - break; - case PeerPushDebitStatus.SuspendedCreatePurse: - newStatus = PeerPushDebitStatus.PendingCreatePurse; - break; - case PeerPushDebitStatus.PendingCreatePurse: - case PeerPushDebitStatus.AbortingRefreshDeleted: - case PeerPushDebitStatus.AbortingRefreshExpired: - case PeerPushDebitStatus.AbortingDeletePurse: - case PeerPushDebitStatus.PendingReady: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Expired: - // Do nothing - break; - default: - assertUnreachable(pushDebitRec.status); - } - if (newStatus != null) { - const oldTxState = computePeerPushDebitTransactionState(pushDebitRec); - pushDebitRec.status = newStatus; - const newTxState = computePeerPushDebitTransactionState(pushDebitRec); - await tx.peerPushDebit.put(pushDebitRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - export function computePeerPushDebitTransactionState( ppiRecord: PeerPushDebitRecord, ): TransactionState { diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 390433f66..fc2508cd3 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -98,6 +98,8 @@ import { makeCoinsVisible, TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, } from "./common.js"; import { fetchFreshExchange } from "./exchanges.js"; import { @@ -107,6 +109,152 @@ import { const logger = new Logger("refresh.ts"); +export class RefreshTransactionContext implements TransactionContext { + public transactionId: string; + + constructor( + public ws: InternalWalletState, + public refreshGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Refresh, + refreshGroupId, + }); + } + + async deleteTransaction(): Promise { + const refreshGroupId = this.refreshGroupId; + const ws = this.ws; + await ws.db + .mktx((x) => [x.refreshGroups, x.tombstones]) + .runReadWrite(async (tx) => { + const rg = await tx.refreshGroups.get(refreshGroupId); + if (rg) { + await tx.refreshGroups.delete(refreshGroupId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId, + }); + } + }); + } + + async suspendTransaction(): Promise { + const { ws, refreshGroupId, transactionId } = this; + let res = await ws.db + .mktx((x) => [x.refreshGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.refreshGroups.get(refreshGroupId); + if (!dg) { + logger.warn( + `can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`, + ); + return undefined; + } + const oldState = computeRefreshTransactionState(dg); + switch (dg.operationStatus) { + case RefreshOperationStatus.Finished: + return undefined; + case RefreshOperationStatus.Pending: { + dg.operationStatus = RefreshOperationStatus.Suspended; + await tx.refreshGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeRefreshTransactionState(dg), + }; + } + case RefreshOperationStatus.Suspended: + return undefined; + } + return undefined; + }); + if (res) { + ws.notify({ + type: NotificationType.TransactionStateTransition, + transactionId, + oldTxState: res.oldTxState, + newTxState: res.newTxState, + }); + } + } + + async abortTransaction(): Promise { + // Refresh transactions only support fail, not abort. + throw new Error("refresh transactions cannot be aborted"); + } + + async resumeTransaction(): Promise { + const { ws, refreshGroupId, transactionId } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.refreshGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.refreshGroups.get(refreshGroupId); + if (!dg) { + logger.warn( + `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`, + ); + return; + } + const oldState = computeRefreshTransactionState(dg); + switch (dg.operationStatus) { + case RefreshOperationStatus.Finished: + return; + case RefreshOperationStatus.Pending: { + return; + } + case RefreshOperationStatus.Suspended: + dg.operationStatus = RefreshOperationStatus.Pending; + await tx.refreshGroups.put(dg); + return { + oldTxState: oldState, + newTxState: computeRefreshTransactionState(dg), + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, refreshGroupId, transactionId } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.refreshGroups]) + .runReadWrite(async (tx) => { + const dg = await tx.refreshGroups.get(refreshGroupId); + if (!dg) { + logger.warn( + `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`, + ); + return; + } + const oldState = computeRefreshTransactionState(dg); + let newStatus: RefreshOperationStatus | undefined; + switch (dg.operationStatus) { + case RefreshOperationStatus.Finished: + break; + case RefreshOperationStatus.Pending: + case RefreshOperationStatus.Suspended: + newStatus = RefreshOperationStatus.Failed; + break; + case RefreshOperationStatus.Failed: + break; + default: + assertUnreachable(dg.operationStatus); + } + if (newStatus) { + dg.operationStatus = newStatus; + await tx.refreshGroups.put(dg); + } + return { + oldTxState: oldState, + newTxState: computeRefreshTransactionState(dg), + }; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } +} + /** * Get the amount that we lose when refreshing a coin of the given denomination * with a certain amount left. @@ -1256,9 +1404,6 @@ export async function autoRefresh( `created refresh group for auto-refresh (${res.refreshGroupId})`, ); } - // logger.trace( - // `current wallet time: ${AbsoluteTime.toIsoString(AbsoluteTime.now())}`, - // ); logger.trace( `next refresh check at ${AbsoluteTime.toIsoString(minCheckThreshold)}`, ); @@ -1308,142 +1453,6 @@ export function computeRefreshTransactionActions( } } -export async function suspendRefreshGroup( - ws: InternalWalletState, - refreshGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Refresh, - refreshGroupId, - }); - let res = await ws.db - .mktx((x) => [x.refreshGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.refreshGroups.get(refreshGroupId); - if (!dg) { - logger.warn( - `can't suspend refresh group, refreshGroupId=${refreshGroupId} not found`, - ); - return undefined; - } - const oldState = computeRefreshTransactionState(dg); - switch (dg.operationStatus) { - case RefreshOperationStatus.Finished: - return undefined; - case RefreshOperationStatus.Pending: { - dg.operationStatus = RefreshOperationStatus.Suspended; - await tx.refreshGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeRefreshTransactionState(dg), - }; - } - case RefreshOperationStatus.Suspended: - return undefined; - } - return undefined; - }); - if (res) { - ws.notify({ - type: NotificationType.TransactionStateTransition, - transactionId, - oldTxState: res.oldTxState, - newTxState: res.newTxState, - }); - } -} - -export async function resumeRefreshGroup( - ws: InternalWalletState, - refreshGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Refresh, - refreshGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.refreshGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.refreshGroups.get(refreshGroupId); - if (!dg) { - logger.warn( - `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`, - ); - return; - } - const oldState = computeRefreshTransactionState(dg); - switch (dg.operationStatus) { - case RefreshOperationStatus.Finished: - return; - case RefreshOperationStatus.Pending: { - return; - } - case RefreshOperationStatus.Suspended: - dg.operationStatus = RefreshOperationStatus.Pending; - await tx.refreshGroups.put(dg); - return { - oldTxState: oldState, - newTxState: computeRefreshTransactionState(dg), - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failRefreshGroup( - ws: InternalWalletState, - refreshGroupId: string, -): Promise { - throw Error("action cancel-aborting not allowed on refreshes"); -} - -export async function abortRefreshGroup( - ws: InternalWalletState, - refreshGroupId: string, -): Promise { - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Refresh, - refreshGroupId, - }); - const transitionInfo = await ws.db - .mktx((x) => [x.refreshGroups]) - .runReadWrite(async (tx) => { - const dg = await tx.refreshGroups.get(refreshGroupId); - if (!dg) { - logger.warn( - `can't resume refresh group, refreshGroupId=${refreshGroupId} not found`, - ); - return; - } - const oldState = computeRefreshTransactionState(dg); - let newStatus: RefreshOperationStatus | undefined; - switch (dg.operationStatus) { - case RefreshOperationStatus.Finished: - break; - case RefreshOperationStatus.Pending: - case RefreshOperationStatus.Suspended: - newStatus = RefreshOperationStatus.Failed; - break; - case RefreshOperationStatus.Failed: - break; - default: - assertUnreachable(dg.operationStatus); - } - if (newStatus) { - dg.operationStatus = newStatus; - await tx.refreshGroups.put(dg); - } - return { - oldTxState: oldState, - newTxState: computeRefreshTransactionState(dg), - }; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - export async function forceRefresh( ws: InternalWalletState, req: ForceRefreshRequest, diff --git a/packages/taler-wallet-core/src/operations/reward.ts b/packages/taler-wallet-core/src/operations/reward.ts index 79beb6432..62ac81d7f 100644 --- a/packages/taler-wallet-core/src/operations/reward.ts +++ b/packages/taler-wallet-core/src/operations/reward.ts @@ -68,6 +68,8 @@ import { makeCoinsVisible, TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, } from "./common.js"; import { fetchFreshExchange } from "./exchanges.js"; import { @@ -86,6 +88,202 @@ import { assertUnreachable } from "../util/assertUnreachable.js"; const logger = new Logger("operations/tip.ts"); +export class RewardTransactionContext implements TransactionContext { + public transactionId: string; + public retryTag: string; + + constructor( + public ws: InternalWalletState, + public walletRewardId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Reward, + walletRewardId, + }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.RewardPickup, + walletRewardId, + }); + } + + async deleteTransaction(): Promise { + const { ws, walletRewardId } = this; + await ws.db + .mktx((x) => [x.rewards, x.tombstones]) + .runReadWrite(async (tx) => { + const tipRecord = await tx.rewards.get(walletRewardId); + if (tipRecord) { + await tx.rewards.delete(walletRewardId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteReward + ":" + walletRewardId, + }); + } + }); + } + + async suspendTransaction(): Promise { + const { ws, walletRewardId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRec = await tx.rewards.get(walletRewardId); + if (!tipRec) { + logger.warn(`transaction tip ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (tipRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.SuspendedPickup: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.DialogAccept: + case RewardRecordStatus.Failed: + break; + case RewardRecordStatus.PendingPickup: + newStatus = RewardRecordStatus.SuspendedPickup; + break; + + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(tipRec); + await tx.rewards.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async abortTransaction(): Promise { + const { ws, walletRewardId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRec = await tx.rewards.get(walletRewardId); + if (!tipRec) { + logger.warn(`transaction tip ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (tipRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.PendingPickup: + case RewardRecordStatus.DialogAccept: + case RewardRecordStatus.Failed: + break; + case RewardRecordStatus.SuspendedPickup: + newStatus = RewardRecordStatus.Aborted; + break; + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(tipRec); + await tx.rewards.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, walletRewardId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const rewardRec = await tx.rewards.get(walletRewardId); + if (!rewardRec) { + logger.warn(`transaction reward ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (rewardRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.PendingPickup: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.DialogAccept: + case RewardRecordStatus.Failed: + break; + case RewardRecordStatus.SuspendedPickup: + newStatus = RewardRecordStatus.PendingPickup; + break; + default: + assertUnreachable(rewardRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(rewardRec); + rewardRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(rewardRec); + await tx.rewards.put(rewardRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, walletRewardId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.rewards]) + .runReadWrite(async (tx) => { + const tipRec = await tx.rewards.get(walletRewardId); + if (!tipRec) { + logger.warn(`transaction tip ${walletRewardId} not found`); + return; + } + let newStatus: RewardRecordStatus | undefined = undefined; + switch (tipRec.status) { + case RewardRecordStatus.Done: + case RewardRecordStatus.Aborted: + case RewardRecordStatus.Failed: + break; + case RewardRecordStatus.PendingPickup: + case RewardRecordStatus.DialogAccept: + case RewardRecordStatus.SuspendedPickup: + newStatus = RewardRecordStatus.Failed; + break; + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeRewardTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeRewardTransactionStatus(tipRec); + await tx.rewards.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); + } +} + /** * Get the (DD37-style) transaction status based on the * database record of a reward. @@ -117,6 +315,10 @@ export function computeRewardTransactionStatus( major: TransactionMajorState.Pending, minor: TransactionMinorState.Pickup, }; + case RewardRecordStatus.Failed: + return { + major: TransactionMajorState.Failed, + }; default: assertUnreachable(tipRecord.status); } @@ -128,6 +330,8 @@ export function computeTipTransactionActions( switch (tipRecord.status) { case RewardRecordStatus.Done: return [TransactionAction.Delete]; + case RewardRecordStatus.Failed: + return [TransactionAction.Delete]; case RewardRecordStatus.Aborted: return [TransactionAction.Delete]; case RewardRecordStatus.PendingPickup: @@ -141,7 +345,7 @@ export function computeTipTransactionActions( } } -export async function prepareTip( +export async function prepareReward( ws: InternalWalletState, talerTipUri: string, ): Promise { @@ -166,33 +370,33 @@ export async function prepareTip( ); logger.trace("checking tip status from", tipStatusUrl.href); const merchantResp = await ws.http.fetch(tipStatusUrl.href); - const tipPickupStatus = await readSuccessResponseJsonOrThrow( + const rewardPickupStatus = await readSuccessResponseJsonOrThrow( merchantResp, codecForRewardPickupGetResponse(), ); - logger.trace(`status ${j2s(tipPickupStatus)}`); + logger.trace(`status ${j2s(rewardPickupStatus)}`); - const amount = Amounts.parseOrThrow(tipPickupStatus.reward_amount); + const amount = Amounts.parseOrThrow(rewardPickupStatus.reward_amount); const currency = amount.currency; logger.trace("new tip, creating tip record"); - await fetchFreshExchange(ws, tipPickupStatus.exchange_url); + await fetchFreshExchange(ws, rewardPickupStatus.exchange_url); //FIXME: is this needed? withdrawDetails is not used // * if the intention is to update the exchange information in the database // maybe we can use another name. `get` seems like a pure-function const withdrawDetails = await getExchangeWithdrawalInfo( ws, - tipPickupStatus.exchange_url, + rewardPickupStatus.exchange_url, amount, undefined, ); - const walletTipId = encodeCrock(getRandomBytes(32)); - await updateWithdrawalDenoms(ws, tipPickupStatus.exchange_url); + const walletRewardId = encodeCrock(getRandomBytes(32)); + await updateWithdrawalDenoms(ws, rewardPickupStatus.exchange_url); const denoms = await getCandidateWithdrawalDenoms( ws, - tipPickupStatus.exchange_url, + rewardPickupStatus.exchange_url, currency, ); const selectedDenoms = selectWithdrawalDenominations(amount, denoms); @@ -201,13 +405,13 @@ export async function prepareTip( const denomSelUid = encodeCrock(getRandomBytes(32)); const newTipRecord: RewardRecord = { - walletRewardId: walletTipId, + walletRewardId: walletRewardId, acceptedTimestamp: undefined, status: RewardRecordStatus.DialogAccept, rewardAmountRaw: Amounts.stringify(amount), - rewardExpiration: timestampProtocolToDb(tipPickupStatus.expiration), - exchangeBaseUrl: tipPickupStatus.exchange_url, - next_url: tipPickupStatus.next_url, + rewardExpiration: timestampProtocolToDb(rewardPickupStatus.expiration), + exchangeBaseUrl: rewardPickupStatus.exchange_url, + next_url: rewardPickupStatus.next_url, merchantBaseUrl: res.merchantBaseUrl, createdTimestamp: timestampPreciseToDb(TalerPreciseTimestamp.now()), merchantRewardId: res.merchantRewardId, @@ -485,160 +689,3 @@ export async function acceptTip( next_url: tipRecord.next_url, }; } - -export async function suspendRewardTransaction( - ws: InternalWalletState, - walletRewardId: string, -): Promise { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.RewardPickup, - walletRewardId: walletRewardId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Reward, - walletRewardId: walletRewardId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.rewards]) - .runReadWrite(async (tx) => { - const tipRec = await tx.rewards.get(walletRewardId); - if (!tipRec) { - logger.warn(`transaction tip ${walletRewardId} not found`); - return; - } - let newStatus: RewardRecordStatus | undefined = undefined; - switch (tipRec.status) { - case RewardRecordStatus.Done: - case RewardRecordStatus.SuspendedPickup: - case RewardRecordStatus.Aborted: - case RewardRecordStatus.DialogAccept: - break; - case RewardRecordStatus.PendingPickup: - newStatus = RewardRecordStatus.SuspendedPickup; - break; - - default: - assertUnreachable(tipRec.status); - } - if (newStatus != null) { - const oldTxState = computeRewardTransactionStatus(tipRec); - tipRec.status = newStatus; - const newTxState = computeRewardTransactionStatus(tipRec); - await tx.rewards.put(tipRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function resumeTipTransaction( - ws: InternalWalletState, - walletRewardId: string, -): Promise { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.RewardPickup, - walletRewardId: walletRewardId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Reward, - walletRewardId: walletRewardId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.rewards]) - .runReadWrite(async (tx) => { - const rewardRec = await tx.rewards.get(walletRewardId); - if (!rewardRec) { - logger.warn(`transaction reward ${walletRewardId} not found`); - return; - } - let newStatus: RewardRecordStatus | undefined = undefined; - switch (rewardRec.status) { - case RewardRecordStatus.Done: - case RewardRecordStatus.PendingPickup: - case RewardRecordStatus.Aborted: - case RewardRecordStatus.DialogAccept: - break; - case RewardRecordStatus.SuspendedPickup: - newStatus = RewardRecordStatus.PendingPickup; - break; - default: - assertUnreachable(rewardRec.status); - } - if (newStatus != null) { - const oldTxState = computeRewardTransactionStatus(rewardRec); - rewardRec.status = newStatus; - const newTxState = computeRewardTransactionStatus(rewardRec); - await tx.rewards.put(rewardRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} - -export async function failTipTransaction( - ws: InternalWalletState, - walletTipId: string, -): Promise { - // We don't have an "aborting" state, so this should never happen! - throw Error("can't run cance-aborting on tip transaction"); -} - -export async function abortTipTransaction( - ws: InternalWalletState, - walletRewardId: string, -): Promise { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.RewardPickup, - walletRewardId: walletRewardId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Reward, - walletRewardId: walletRewardId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.rewards]) - .runReadWrite(async (tx) => { - const tipRec = await tx.rewards.get(walletRewardId); - if (!tipRec) { - logger.warn(`transaction tip ${walletRewardId} not found`); - return; - } - let newStatus: RewardRecordStatus | undefined = undefined; - switch (tipRec.status) { - case RewardRecordStatus.Done: - case RewardRecordStatus.Aborted: - case RewardRecordStatus.PendingPickup: - case RewardRecordStatus.DialogAccept: - break; - case RewardRecordStatus.SuspendedPickup: - newStatus = RewardRecordStatus.Aborted; - break; - default: - assertUnreachable(tipRec.status); - } - if (newStatus != null) { - const oldTxState = computeRewardTransactionStatus(tipRec); - tipRec.status = newStatus; - const newTxState = computeRewardTransactionStatus(tipRec); - await tx.rewards.put(tipRec); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, transitionInfo); -} diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 142eff7c1..908aa540a 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -79,61 +79,45 @@ import { constructTaskIdentifier, resetPendingTaskTimeout, TaskIdentifiers, - TombstoneTag, + TransactionContext, } from "./common.js"; import { - abortDepositGroup, computeDepositTransactionActions, computeDepositTransactionStatus, - deleteDepositGroup, - failDepositTransaction, - resumeDepositGroup, - suspendDepositGroup, + DepositTransactionContext, } from "./deposits.js"; import { ExchangeWireDetails, getExchangeWireDetailsInTx, } from "./exchanges.js"; import { - abortPayMerchant, computePayMerchantTransactionActions, computePayMerchantTransactionState, computeRefundTransactionState, expectProposalDownload, extractContractData, - failPaymentTransaction, - resumePayMerchant, - suspendPayMerchant, + PayMerchantTransactionContext, + RefundTransactionContext, } from "./pay-merchant.js"; import { - abortPeerPullCreditTransaction, computePeerPullCreditTransactionActions, computePeerPullCreditTransactionState, - failPeerPullCreditTransaction, - resumePeerPullCreditTransaction, - suspendPeerPullCreditTransaction, + PeerPullCreditTransactionContext, } from "./pay-peer-pull-credit.js"; import { computePeerPullDebitTransactionActions, computePeerPullDebitTransactionState, PeerPullDebitTransactionContext, - suspendPeerPullDebitTransaction, } from "./pay-peer-pull-debit.js"; import { - abortPeerPushCreditTransaction, computePeerPushCreditTransactionActions, computePeerPushCreditTransactionState, - failPeerPushCreditTransaction, - resumePeerPushCreditTransaction, - suspendPeerPushCreditTransaction, + PeerPushCreditTransactionContext, } from "./pay-peer-push-credit.js"; import { - abortPeerPushDebitTransaction, computePeerPushDebitTransactionActions, computePeerPushDebitTransactionState, - failPeerPushDebitTransaction, - resumePeerPushDebitTransaction, - suspendPeerPushDebitTransaction, + PeerPushDebitTransactionContext, } from "./pay-peer-push-debit.js"; import { iterRecordsForDeposit, @@ -148,29 +132,20 @@ import { iterRecordsForWithdrawal, } from "./pending.js"; import { - abortRefreshGroup, computeRefreshTransactionActions, computeRefreshTransactionState, - failRefreshGroup, - resumeRefreshGroup, - suspendRefreshGroup, + RefreshTransactionContext, } from "./refresh.js"; import { - abortTipTransaction, computeRewardTransactionStatus, computeTipTransactionActions, - failTipTransaction, - resumeTipTransaction, - suspendRewardTransaction, + RewardTransactionContext, } from "./reward.js"; import { - abortWithdrawalTransaction, augmentPaytoUrisForWithdrawal, computeWithdrawalTransactionActions, computeWithdrawalTransactionStatus, - failWithdrawalTransaction, - resumeWithdrawalTransaction, - suspendWithdrawalTransaction, + WithdrawTransactionContext, } from "./withdraw.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -1565,100 +1540,61 @@ export async function retryTransaction( } } -/** - * Suspends a pending transaction, stopping any associated network activities, - * but with a chance of trying again at a later time. This could be useful if - * a user needs to save battery power or bandwidth and an operation is expected - * to take longer (such as a backup, recovery or very large withdrawal operation). - */ -export async function suspendTransaction( +async function getContextForTransaction( ws: InternalWalletState, transactionId: string, -): Promise { +): Promise { const tx = parseTransactionIdentifier(transactionId); if (!tx) { throw Error("invalid transaction ID"); } switch (tx.tag) { case TransactionType.Deposit: - await suspendDepositGroup(ws, tx.depositGroupId); - return; + return new DepositTransactionContext(ws, tx.depositGroupId); case TransactionType.Refresh: - await suspendRefreshGroup(ws, tx.refreshGroupId); - return; + return new RefreshTransactionContext(ws, tx.refreshGroupId); case TransactionType.InternalWithdrawal: case TransactionType.Withdrawal: - await suspendWithdrawalTransaction(ws, tx.withdrawalGroupId); - return; + return new WithdrawTransactionContext(ws, tx.withdrawalGroupId); case TransactionType.Payment: - await suspendPayMerchant(ws, tx.proposalId); - return; + return new PayMerchantTransactionContext(ws, tx.proposalId); case TransactionType.PeerPullCredit: - await suspendPeerPullCreditTransaction(ws, tx.pursePub); - break; + return new PeerPullCreditTransactionContext(ws, tx.pursePub); case TransactionType.PeerPushDebit: - await suspendPeerPushDebitTransaction(ws, tx.pursePub); - break; + return new PeerPushDebitTransactionContext(ws, tx.pursePub); case TransactionType.PeerPullDebit: - await suspendPeerPullDebitTransaction(ws, tx.peerPullDebitId); - break; + return new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId); case TransactionType.PeerPushCredit: - await suspendPeerPushCreditTransaction(ws, tx.peerPushCreditId); - break; + return new PeerPushCreditTransactionContext(ws, tx.peerPushCreditId); case TransactionType.Refund: - throw Error("refund transactions can't be suspended or resumed"); + return new RefundTransactionContext(ws, tx.refundGroupId); case TransactionType.Reward: - await suspendRewardTransaction(ws, tx.walletRewardId); - break; + return new RewardTransactionContext(ws, tx.walletRewardId); default: assertUnreachable(tx); } } +/** + * Suspends a pending transaction, stopping any associated network activities, + * but with a chance of trying again at a later time. This could be useful if + * a user needs to save battery power or bandwidth and an operation is expected + * to take longer (such as a backup, recovery or very large withdrawal operation). + */ +export async function suspendTransaction( + ws: InternalWalletState, + transactionId: string, +): Promise { + const ctx = await getContextForTransaction(ws, transactionId); + await ctx.suspendTransaction(); +} + export async function failTransaction( ws: InternalWalletState, transactionId: string, ): Promise { - const tx = parseTransactionIdentifier(transactionId); - if (!tx) { - throw Error("invalid transaction ID"); - } - switch (tx.tag) { - case TransactionType.Deposit: - await failDepositTransaction(ws, tx.depositGroupId); - return; - case TransactionType.InternalWithdrawal: - case TransactionType.Withdrawal: - await failWithdrawalTransaction(ws, tx.withdrawalGroupId); - return; - case TransactionType.Payment: - await failPaymentTransaction(ws, tx.proposalId); - return; - case TransactionType.Refund: - throw Error("can't do cancel-aborting on refund transaction"); - case TransactionType.Reward: - await failTipTransaction(ws, tx.walletRewardId); - return; - case TransactionType.Refresh: - await failRefreshGroup(ws, tx.refreshGroupId); - return; - case TransactionType.PeerPullCredit: - await failPeerPullCreditTransaction(ws, tx.pursePub); - return; - case TransactionType.PeerPullDebit: { - const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId); - await ctx.failTransaction(); - return; - } - case TransactionType.PeerPushCredit: - await failPeerPushCreditTransaction(ws, tx.peerPushCreditId); - return; - case TransactionType.PeerPushDebit: - await failPeerPushDebitTransaction(ws, tx.pursePub); - return; - default: - assertUnreachable(tx); - } + const ctx = await getContextForTransaction(ws, transactionId); + await ctx.failTransaction(); } /** @@ -1668,44 +1604,8 @@ export async function resumeTransaction( ws: InternalWalletState, transactionId: string, ): Promise { - const tx = parseTransactionIdentifier(transactionId); - if (!tx) { - throw Error("invalid transaction ID"); - } - switch (tx.tag) { - case TransactionType.Deposit: - await resumeDepositGroup(ws, tx.depositGroupId); - return; - case TransactionType.Refresh: - await resumeRefreshGroup(ws, tx.refreshGroupId); - return; - case TransactionType.InternalWithdrawal: - case TransactionType.Withdrawal: - await resumeWithdrawalTransaction(ws, tx.withdrawalGroupId); - return; - case TransactionType.Payment: - await resumePayMerchant(ws, tx.proposalId); - return; - case TransactionType.PeerPullCredit: - await resumePeerPullCreditTransaction(ws, tx.pursePub); - break; - case TransactionType.PeerPushDebit: - await resumePeerPushDebitTransaction(ws, tx.pursePub); - break; - case TransactionType.PeerPullDebit: { - const ctx = new PeerPullDebitTransactionContext(ws, tx.peerPullDebitId); - await ctx.resumeTransaction(); - return; - } - case TransactionType.PeerPushCredit: - await resumePeerPushCreditTransaction(ws, tx.peerPushCreditId); - break; - case TransactionType.Refund: - throw Error("refund transactions can't be suspended or resumed"); - case TransactionType.Reward: - await resumeTipTransaction(ws, tx.walletRewardId); - break; - } + const ctx = await getContextForTransaction(ws, transactionId); + await ctx.resumeTransaction(); } /** @@ -1715,244 +1615,16 @@ export async function deleteTransaction( ws: InternalWalletState, transactionId: string, ): Promise { - const parsedTx = parseTransactionIdentifier(transactionId); - - if (!parsedTx) { - throw Error("invalid transaction ID"); - } - - switch (parsedTx.tag) { - case TransactionType.PeerPushCredit: { - const peerPushCreditId = parsedTx.peerPushCreditId; - await ws.db - .mktx((x) => [x.withdrawalGroups, x.peerPushCredit, x.tombstones]) - .runReadWrite(async (tx) => { - const pushInc = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushInc) { - return; - } - if (pushInc.withdrawalGroupId) { - const withdrawalGroupId = pushInc.withdrawalGroupId; - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: - TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - } - } - await tx.peerPushCredit.delete(peerPushCreditId); - await tx.tombstones.put({ - id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId, - }); - }); - return; - } - - case TransactionType.PeerPullCredit: { - const pursePub = parsedTx.pursePub; - await ws.db - .mktx((x) => [x.withdrawalGroups, x.peerPullCredit, x.tombstones]) - .runReadWrite(async (tx) => { - const pullIni = await tx.peerPullCredit.get(pursePub); - if (!pullIni) { - return; - } - if (pullIni.withdrawalGroupId) { - const withdrawalGroupId = pullIni.withdrawalGroupId; - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: - TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - } - } - await tx.peerPullCredit.delete(pursePub); - await tx.tombstones.put({ - id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, - }); - }); - - return; - } - - case TransactionType.Withdrawal: { - const withdrawalGroupId = parsedTx.withdrawalGroupId; - await ws.db - .mktx((x) => [x.withdrawalGroups, x.tombstones]) - .runReadWrite(async (tx) => { - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - return; - } - }); - return; - } - - case TransactionType.Payment: { - const proposalId = parsedTx.proposalId; - await ws.db - .mktx((x) => [x.purchases, x.tombstones]) - .runReadWrite(async (tx) => { - let found = false; - const purchase = await tx.purchases.get(proposalId); - if (purchase) { - found = true; - await tx.purchases.delete(proposalId); - } - if (found) { - await tx.tombstones.put({ - id: TombstoneTag.DeletePayment + ":" + proposalId, - }); - } - }); - return; - } - - case TransactionType.Refresh: { - const refreshGroupId = parsedTx.refreshGroupId; - await ws.db - .mktx((x) => [x.refreshGroups, x.tombstones]) - .runReadWrite(async (tx) => { - const rg = await tx.refreshGroups.get(refreshGroupId); - if (rg) { - await tx.refreshGroups.delete(refreshGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteRefreshGroup + ":" + refreshGroupId, - }); - } - }); - - return; - } - - case TransactionType.Reward: { - const tipId = parsedTx.walletRewardId; - await ws.db - .mktx((x) => [x.rewards, x.tombstones]) - .runReadWrite(async (tx) => { - const tipRecord = await tx.rewards.get(tipId); - if (tipRecord) { - await tx.rewards.delete(tipId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteReward + ":" + tipId, - }); - } - }); - return; - } - - case TransactionType.Deposit: { - const depositGroupId = parsedTx.depositGroupId; - await deleteDepositGroup(ws, depositGroupId); - return; - } - - case TransactionType.Refund: { - const refundGroupId = parsedTx.refundGroupId; - await ws.db - .mktx((x) => [x.refundGroups, x.tombstones]) - .runReadWrite(async (tx) => { - const refundRecord = await tx.refundGroups.get(refundGroupId); - if (!refundRecord) { - return; - } - await tx.refundGroups.delete(refundGroupId); - await tx.tombstones.put({ id: transactionId }); - // FIXME: Also tombstone the refund items, so that they won't reappear. - }); - return; - } - - case TransactionType.PeerPullDebit: { - const peerPullDebitId = parsedTx.peerPullDebitId; - await ws.db - .mktx((x) => [x.peerPullDebit, x.tombstones]) - .runReadWrite(async (tx) => { - const debit = await tx.peerPullDebit.get(peerPullDebitId); - if (debit) { - await tx.peerPullDebit.delete(peerPullDebitId); - await tx.tombstones.put({ id: transactionId }); - } - }); - - return; - } - - case TransactionType.PeerPushDebit: { - const pursePub = parsedTx.pursePub; - await ws.db - .mktx((x) => [x.peerPushDebit, x.tombstones]) - .runReadWrite(async (tx) => { - const debit = await tx.peerPushDebit.get(pursePub); - if (debit) { - await tx.peerPushDebit.delete(pursePub); - await tx.tombstones.put({ id: transactionId }); - } - }); - return; - } - } + const ctx = await getContextForTransaction(ws, transactionId); + await ctx.deleteTransaction(); } export async function abortTransaction( ws: InternalWalletState, transactionId: string, ): Promise { - const txId = parseTransactionIdentifier(transactionId); - if (!txId) { - throw Error("invalid transaction identifier"); - } - - switch (txId.tag) { - case TransactionType.Payment: { - await abortPayMerchant(ws, txId.proposalId); - break; - } - case TransactionType.Withdrawal: - case TransactionType.InternalWithdrawal: { - await abortWithdrawalTransaction(ws, txId.withdrawalGroupId); - break; - } - case TransactionType.Deposit: - await abortDepositGroup(ws, txId.depositGroupId); - break; - case TransactionType.Reward: - await abortTipTransaction(ws, txId.walletRewardId); - break; - case TransactionType.Refund: - throw Error("can't abort refund transactions"); - case TransactionType.Refresh: - await abortRefreshGroup(ws, txId.refreshGroupId); - break; - case TransactionType.PeerPullCredit: - await abortPeerPullCreditTransaction(ws, txId.pursePub); - break; - case TransactionType.PeerPullDebit: { - const ctx = new PeerPullDebitTransactionContext(ws, txId.peerPullDebitId); - await ctx.abortTransaction(); - return; - } - case TransactionType.PeerPushCredit: - await abortPeerPushCreditTransaction(ws, txId.peerPushCreditId); - break; - case TransactionType.PeerPushDebit: - await abortPeerPushDebitTransaction(ws, txId.pursePub); - break; - default: { - assertUnreachable(txId); - } - } + const ctx = await getContextForTransaction(ws, transactionId); + await ctx.abortTransaction(); } export interface TransitionInfo { diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index d02cf0597..58df75964 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -105,6 +105,8 @@ import { TaskIdentifiers, TaskRunResult, TaskRunResultType, + TombstoneTag, + TransactionContext, constructTaskIdentifier, makeCoinAvailable, makeCoinsVisible, @@ -146,246 +148,246 @@ import { */ const logger = new Logger("operations/withdraw.ts"); -export async function suspendWithdrawalTransaction( - ws: InternalWalletState, - withdrawalGroupId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.Withdraw, - withdrawalGroupId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.withdrawalGroups]) - .runReadWrite(async (tx) => { - const wg = await tx.withdrawalGroups.get(withdrawalGroupId); - if (!wg) { - logger.warn(`withdrawal group ${withdrawalGroupId} not found`); - return; - } - let newStatus: WithdrawalGroupStatus | undefined = undefined; - switch (wg.status) { - case WithdrawalGroupStatus.PendingReady: - newStatus = WithdrawalGroupStatus.SuspendedReady; - break; - case WithdrawalGroupStatus.AbortingBank: - newStatus = WithdrawalGroupStatus.SuspendedAbortingBank; - break; - case WithdrawalGroupStatus.PendingWaitConfirmBank: - newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank; - break; - case WithdrawalGroupStatus.PendingRegisteringBank: - newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank; - break; - case WithdrawalGroupStatus.PendingQueryingStatus: - newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus; - break; - case WithdrawalGroupStatus.PendingKyc: - newStatus = WithdrawalGroupStatus.SuspendedKyc; - break; - case WithdrawalGroupStatus.PendingAml: - newStatus = WithdrawalGroupStatus.SuspendedAml; - break; - default: - logger.warn( - `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`, - ); - } - if (newStatus != null) { - const oldTxState = computeWithdrawalTransactionStatus(wg); - wg.status = newStatus; - const newTxState = computeWithdrawalTransactionStatus(wg); - await tx.withdrawalGroups.put(wg); - return { - oldTxState, - newTxState, - }; - } - return undefined; +export class WithdrawTransactionContext implements TransactionContext { + public transactionId: string; + public retryTag: string; + + constructor( + public ws: InternalWalletState, + public withdrawalGroupId: string, + ) { + this.transactionId = constructTransactionIdentifier({ + tag: TransactionType.Withdrawal, + withdrawalGroupId, }); + this.retryTag = constructTaskIdentifier({ + tag: PendingTaskType.Withdraw, + withdrawalGroupId, + }); + } - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); - notifyTransition(ws, transactionId, transitionInfo); -} + async deleteTransaction(): Promise { + const { ws, withdrawalGroupId } = this; + await ws.db + .mktx((x) => [x.withdrawalGroups, x.tombstones]) + .runReadWrite(async (tx) => { + const withdrawalGroupRecord = + await tx.withdrawalGroups.get(withdrawalGroupId); + if (withdrawalGroupRecord) { + await tx.withdrawalGroups.delete(withdrawalGroupId); + await tx.tombstones.put({ + id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, + }); + return; + } + }); + } -export async function resumeWithdrawalTransaction( - ws: InternalWalletState, - withdrawalGroupId: string, -) { - const transitionInfo = await ws.db - .mktx((x) => [x.withdrawalGroups]) - .runReadWrite(async (tx) => { - const wg = await tx.withdrawalGroups.get(withdrawalGroupId); - if (!wg) { - logger.warn(`withdrawal group ${withdrawalGroupId} not found`); - return; - } - let newStatus: WithdrawalGroupStatus | undefined = undefined; - switch (wg.status) { - case WithdrawalGroupStatus.SuspendedReady: - newStatus = WithdrawalGroupStatus.PendingReady; - break; - case WithdrawalGroupStatus.SuspendedAbortingBank: - newStatus = WithdrawalGroupStatus.AbortingBank; - break; - case WithdrawalGroupStatus.SuspendedWaitConfirmBank: - newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank; - break; - case WithdrawalGroupStatus.SuspendedQueryingStatus: - newStatus = WithdrawalGroupStatus.PendingQueryingStatus; - break; - case WithdrawalGroupStatus.SuspendedRegisteringBank: - newStatus = WithdrawalGroupStatus.PendingRegisteringBank; - break; - case WithdrawalGroupStatus.SuspendedAml: - newStatus = WithdrawalGroupStatus.PendingAml; - break; - case WithdrawalGroupStatus.SuspendedKyc: - newStatus = WithdrawalGroupStatus.PendingKyc; - break; - default: - logger.warn( - `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`, - ); - } - if (newStatus != null) { - const oldTxState = computeWithdrawalTransactionStatus(wg); - wg.status = newStatus; - const newTxState = computeWithdrawalTransactionStatus(wg); - await tx.withdrawalGroups.put(wg); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); - notifyTransition(ws, transactionId, transitionInfo); -} + async suspendTransaction(): Promise { + const { ws, withdrawalGroupId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.PendingReady: + newStatus = WithdrawalGroupStatus.SuspendedReady; + break; + case WithdrawalGroupStatus.AbortingBank: + newStatus = WithdrawalGroupStatus.SuspendedAbortingBank; + break; + case WithdrawalGroupStatus.PendingWaitConfirmBank: + newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank; + break; + case WithdrawalGroupStatus.PendingRegisteringBank: + newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank; + break; + case WithdrawalGroupStatus.PendingQueryingStatus: + newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus; + break; + case WithdrawalGroupStatus.PendingKyc: + newStatus = WithdrawalGroupStatus.SuspendedKyc; + break; + case WithdrawalGroupStatus.PendingAml: + newStatus = WithdrawalGroupStatus.SuspendedAml; + break; + default: + logger.warn( + `Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`, + ); + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); -export async function abortWithdrawalTransaction( - ws: InternalWalletState, - withdrawalGroupId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.Withdraw, - withdrawalGroupId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); - stopLongpolling(ws, taskId); - const transitionInfo = await ws.db - .mktx((x) => [x.withdrawalGroups]) - .runReadWrite(async (tx) => { - const wg = await tx.withdrawalGroups.get(withdrawalGroupId); - if (!wg) { - logger.warn(`withdrawal group ${withdrawalGroupId} not found`); - return; - } - let newStatus: WithdrawalGroupStatus | undefined = undefined; - switch (wg.status) { - case WithdrawalGroupStatus.SuspendedRegisteringBank: - case WithdrawalGroupStatus.SuspendedWaitConfirmBank: - case WithdrawalGroupStatus.PendingWaitConfirmBank: - case WithdrawalGroupStatus.PendingRegisteringBank: - newStatus = WithdrawalGroupStatus.AbortingBank; - break; - case WithdrawalGroupStatus.SuspendedAml: - case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.SuspendedQueryingStatus: - case WithdrawalGroupStatus.SuspendedReady: - case WithdrawalGroupStatus.PendingAml: - case WithdrawalGroupStatus.PendingKyc: - case WithdrawalGroupStatus.PendingQueryingStatus: - newStatus = WithdrawalGroupStatus.AbortedExchange; - break; - case WithdrawalGroupStatus.PendingReady: - newStatus = WithdrawalGroupStatus.SuspendedReady; - break; - case WithdrawalGroupStatus.SuspendedAbortingBank: - case WithdrawalGroupStatus.AbortingBank: - // No transition needed, but not an error - break; - case WithdrawalGroupStatus.Done: - case WithdrawalGroupStatus.FailedBankAborted: - case WithdrawalGroupStatus.AbortedExchange: - case WithdrawalGroupStatus.AbortedBank: - case WithdrawalGroupStatus.FailedAbortingBank: - // Not allowed - throw Error("abort not allowed in current state"); - break; - default: - assertUnreachable(wg.status); - } - if (newStatus != null) { - const oldTxState = computeWithdrawalTransactionStatus(wg); - wg.status = newStatus; - const newTxState = computeWithdrawalTransactionStatus(wg); - await tx.withdrawalGroups.put(wg); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - ws.workAvailable.trigger(); - notifyTransition(ws, transactionId, transitionInfo); -} + notifyTransition(ws, transactionId, transitionInfo); + } -export async function failWithdrawalTransaction( - ws: InternalWalletState, - withdrawalGroupId: string, -) { - const taskId = constructTaskIdentifier({ - tag: PendingTaskType.Withdraw, - withdrawalGroupId, - }); - const transactionId = constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId, - }); - stopLongpolling(ws, taskId); - const stateUpdate = await ws.db - .mktx((x) => [x.withdrawalGroups]) - .runReadWrite(async (tx) => { - const wg = await tx.withdrawalGroups.get(withdrawalGroupId); - if (!wg) { - logger.warn(`withdrawal group ${withdrawalGroupId} not found`); - return; - } - let newStatus: WithdrawalGroupStatus | undefined = undefined; - switch (wg.status) { - case WithdrawalGroupStatus.SuspendedAbortingBank: - case WithdrawalGroupStatus.AbortingBank: - newStatus = WithdrawalGroupStatus.FailedAbortingBank; - break; - default: - break; - } - if (newStatus != null) { - const oldTxState = computeWithdrawalTransactionStatus(wg); - wg.status = newStatus; - const newTxState = computeWithdrawalTransactionStatus(wg); - await tx.withdrawalGroups.put(wg); - return { - oldTxState, - newTxState, - }; - } - return undefined; - }); - notifyTransition(ws, transactionId, stateUpdate); + async abortTransaction(): Promise { + const { ws, withdrawalGroupId, transactionId } = this; + stopLongpolling(ws, this.retryTag); + const transitionInfo = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.SuspendedRegisteringBank: + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + case WithdrawalGroupStatus.PendingWaitConfirmBank: + case WithdrawalGroupStatus.PendingRegisteringBank: + newStatus = WithdrawalGroupStatus.AbortingBank; + break; + case WithdrawalGroupStatus.SuspendedAml: + case WithdrawalGroupStatus.SuspendedKyc: + case WithdrawalGroupStatus.SuspendedQueryingStatus: + case WithdrawalGroupStatus.SuspendedReady: + case WithdrawalGroupStatus.PendingAml: + case WithdrawalGroupStatus.PendingKyc: + case WithdrawalGroupStatus.PendingQueryingStatus: + newStatus = WithdrawalGroupStatus.AbortedExchange; + break; + case WithdrawalGroupStatus.PendingReady: + newStatus = WithdrawalGroupStatus.SuspendedReady; + break; + case WithdrawalGroupStatus.SuspendedAbortingBank: + case WithdrawalGroupStatus.AbortingBank: + // No transition needed, but not an error + break; + case WithdrawalGroupStatus.Done: + case WithdrawalGroupStatus.FailedBankAborted: + case WithdrawalGroupStatus.AbortedExchange: + case WithdrawalGroupStatus.AbortedBank: + case WithdrawalGroupStatus.FailedAbortingBank: + // Not allowed + throw Error("abort not allowed in current state"); + break; + default: + assertUnreachable(wg.status); + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async resumeTransaction(): Promise { + const { ws, withdrawalGroupId, transactionId } = this; + const transitionInfo = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.SuspendedReady: + newStatus = WithdrawalGroupStatus.PendingReady; + break; + case WithdrawalGroupStatus.SuspendedAbortingBank: + newStatus = WithdrawalGroupStatus.AbortingBank; + break; + case WithdrawalGroupStatus.SuspendedWaitConfirmBank: + newStatus = WithdrawalGroupStatus.PendingWaitConfirmBank; + break; + case WithdrawalGroupStatus.SuspendedQueryingStatus: + newStatus = WithdrawalGroupStatus.PendingQueryingStatus; + break; + case WithdrawalGroupStatus.SuspendedRegisteringBank: + newStatus = WithdrawalGroupStatus.PendingRegisteringBank; + break; + case WithdrawalGroupStatus.SuspendedAml: + newStatus = WithdrawalGroupStatus.PendingAml; + break; + case WithdrawalGroupStatus.SuspendedKyc: + newStatus = WithdrawalGroupStatus.PendingKyc; + break; + default: + logger.warn( + `Unsupported 'resume' on withdrawal transaction in status ${wg.status}`, + ); + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); + } + + async failTransaction(): Promise { + const { ws, withdrawalGroupId, transactionId, retryTag } = this; + stopLongpolling(ws, retryTag); + const stateUpdate = await ws.db + .mktx((x) => [x.withdrawalGroups]) + .runReadWrite(async (tx) => { + const wg = await tx.withdrawalGroups.get(withdrawalGroupId); + if (!wg) { + logger.warn(`withdrawal group ${withdrawalGroupId} not found`); + return; + } + let newStatus: WithdrawalGroupStatus | undefined = undefined; + switch (wg.status) { + case WithdrawalGroupStatus.SuspendedAbortingBank: + case WithdrawalGroupStatus.AbortingBank: + newStatus = WithdrawalGroupStatus.FailedAbortingBank; + break; + default: + break; + } + if (newStatus != null) { + const oldTxState = computeWithdrawalTransactionStatus(wg); + wg.status = newStatus; + const newTxState = computeWithdrawalTransactionStatus(wg); + await tx.withdrawalGroups.put(wg); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, stateUpdate); + } } export function computeWithdrawalTransactionStatus( @@ -2413,6 +2415,16 @@ export async function internalPerformCreateWithdrawalGroup( exchangeNotif: undefined, }; } + const existingWg = await tx.withdrawalGroups.get( + withdrawalGroup.withdrawalGroupId, + ); + if (existingWg) { + return { + withdrawalGroup: existingWg, + exchangeNotif: undefined, + transitionInfo: undefined, + }; + } await tx.withdrawalGroups.add(withdrawalGroup); await tx.reserves.put({ reservePub: withdrawalGroup.reservePub, diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 5d563f620..17b9b407c 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -581,7 +581,7 @@ export type DbReadOnlyTransactionArr< } : never; -export interface TransactionContext { +export interface DbTransactionContext { runReadWrite(f: ReadWriteTransactionFunction): Promise; runReadOnly(f: ReadOnlyTransactionFunction): Promise; } @@ -804,7 +804,7 @@ export class DbAccess { /** * Run a transaction with all object stores. */ - mktxAll(): TransactionContext { + mktxAll(): DbTransactionContext { const storeNames: string[] = []; const accessibleStores: { [x: string]: StoreWithIndexes } = {}; @@ -904,7 +904,7 @@ export class DbAccess { BoundStores extends { [X in StoreNamesOf]: StoreList[number] & { storeName: X }; }, - >(namePicker: (x: StoreMap) => StoreList): TransactionContext { + >(namePicker: (x: StoreMap) => StoreList): DbTransactionContext { const storeNames: string[] = []; const accessibleStores: { [x: string]: StoreWithIndexes } = {}; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 1fa1d117e..d6da2250a 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -151,7 +151,6 @@ import { CancelFn, InternalWalletState, MerchantInfo, - MerchantOperations, NotificationListener, RecoupOperations, RefreshOperations, @@ -246,7 +245,7 @@ import { import { acceptTip, computeRewardTransactionStatus, - prepareTip, + prepareReward, processTip, } from "./operations/reward.js"; import { @@ -1164,7 +1163,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.PrepareReward: { const req = codecForPrepareRewardRequest().decode(payload); - return await prepareTip(ws, req.talerRewardUri); + return await prepareReward(ws, req.talerRewardUri); } case WalletApiOperation.StartRefundQueryForUri: { const req = codecForPrepareRefundRequest().decode(payload); @@ -1609,9 +1608,6 @@ class InternalWalletStateImpl implements InternalWalletState { createRecoupGroup, }; - merchantOps: MerchantOperations = { - getMerchantInfo, - }; refreshOps: RefreshOperations = { createRefreshGroup, -- cgit v1.2.3