From 0323067c0757262084e16a9bba9d58bcd773fc23 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 30 May 2023 09:33:32 +0200 Subject: wallet-core: add missing resume/suspend implementations --- packages/taler-wallet-core/src/db.ts | 39 ++- .../src/operations/backup/export.ts | 10 +- .../src/operations/pay-merchant.ts | 179 +++++++++-- .../taler-wallet-core/src/operations/pay-peer.ts | 348 ++++++++++++++++++++- packages/taler-wallet-core/src/operations/tip.ts | 108 ++++++- .../src/operations/transactions.ts | 127 +++++++- .../taler-wallet-core/src/operations/withdraw.ts | 2 +- 7 files changed, 755 insertions(+), 58 deletions(-) (limited to 'packages/taler-wallet-core/src') diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 0e35fe27c..3edaf8af5 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -863,11 +863,22 @@ export interface TipRecord { * The url to be redirected after the tip is accepted. */ next_url: string | undefined; + /** * Timestamp for when the wallet finished picking up the tip * from the merchant. */ pickedUpTimestamp: TalerPreciseTimestamp | undefined; + + status: TipRecordStatus; +} + +export enum TipRecordStatus { + PendingPickup = 10, + + SuspendidPickup = 21, + + Done = 50, } export enum RefreshCoinStatus { @@ -1078,12 +1089,12 @@ export enum PurchaseStatus { /** * Not downloaded yet. */ - DownloadingProposal = 10, + PendingDownloadingProposal = 10, /** * The user has accepted the proposal. */ - Paying = 11, + PendingPaying = 11, /** * Currently in the process of aborting with a refund. @@ -1093,17 +1104,17 @@ export enum PurchaseStatus { /** * Paying a second time, likely with different session ID */ - PayingReplay = 13, + PendingPayingReplay = 13, /** * Query for refunds (until query succeeds). */ - QueryingRefund = 14, + PendingQueryingRefund = 14, /** * Query for refund (until auto-refund deadline is reached). */ - QueryingAutoRefund = 15, + PendingQueryingAutoRefund = 15, PendingAcceptRefund = 16, @@ -1902,7 +1913,7 @@ export interface PeerPullPaymentInitiationRecord { export enum PeerPushPaymentIncomingStatus { PendingMerge = 10 /* ACTIVE_START */, - MergeKycRequired = 11 /* ACTIVE_START + 1 */, + PendingMergeKycRequired = 11 /* ACTIVE_START + 1 */, /** * Merge was successful and withdrawal group has been created, now * everything is in the hand of the withdrawal group. @@ -2829,6 +2840,22 @@ export const walletDbFixups: FixupDescription[] = [ }); }, }, + { + name: "TipRecordRecord_status_add", + async fn(tx): Promise { + await tx.tips.iter().forEachAsync(async (r) => { + // Remove legacy transactions that don't have the totalCost field yet. + if (r.status == null) { + if (r.pickedUpTimestamp) { + r.status = TipRecordStatus.Done; + } else { + r.status = TipRecordStatus.PendingPickup; + } + await tx.tips.put(r); + } + }); + }, + }, ]; const logger = new Logger("db.ts"); diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts index ff5f1e177..0aca45551 100644 --- a/packages/taler-wallet-core/src/operations/backup/export.ts +++ b/packages/taler-wallet-core/src/operations/backup/export.ts @@ -412,14 +412,14 @@ export async function exportBackup( let propStatus: BackupProposalStatus; switch (purch.purchaseStatus) { case PurchaseStatus.Done: - case PurchaseStatus.QueryingAutoRefund: - case PurchaseStatus.QueryingRefund: + case PurchaseStatus.PendingQueryingAutoRefund: + case PurchaseStatus.PendingQueryingRefund: propStatus = BackupProposalStatus.Paid; break; - case PurchaseStatus.PayingReplay: - case PurchaseStatus.DownloadingProposal: + case PurchaseStatus.PendingPayingReplay: + case PurchaseStatus.PendingDownloadingProposal: case PurchaseStatus.Proposed: - case PurchaseStatus.Paying: + case PurchaseStatus.PendingPaying: propStatus = BackupProposalStatus.Proposed; break; case PurchaseStatus.FailedClaim: diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 4ea41c695..30c75f695 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -131,6 +131,7 @@ import { import { constructTransactionIdentifier, notifyTransition, + stopLongpolling, } from "./transactions.js"; /** @@ -339,7 +340,7 @@ async function processDownloadProposal( }; } - if (proposal.purchaseStatus != PurchaseStatus.DownloadingProposal) { + if (proposal.purchaseStatus != PurchaseStatus.PendingDownloadingProposal) { return { type: OperationAttemptResultType.Finished, result: undefined, @@ -516,7 +517,7 @@ async function processDownloadProposal( if (!p) { return; } - if (p.purchaseStatus !== PurchaseStatus.DownloadingProposal) { + if (p.purchaseStatus !== PurchaseStatus.PendingDownloadingProposal) { return; } const oldTxState = computePayMerchantTransactionState(p); @@ -626,7 +627,7 @@ async function createPurchase( merchantBaseUrl, orderId, proposalId: proposalId, - purchaseStatus: PurchaseStatus.DownloadingProposal, + purchaseStatus: PurchaseStatus.PendingDownloadingProposal, repurchaseProposalId: undefined, downloadSessionId: sessionId, autoRefundDeadline: undefined, @@ -699,7 +700,7 @@ async function storeFirstPaySuccess( return; } const oldTxState = computePayMerchantTransactionState(purchase); - if (purchase.purchaseStatus === PurchaseStatus.Paying) { + if (purchase.purchaseStatus === PurchaseStatus.PendingPaying) { purchase.purchaseStatus = PurchaseStatus.Done; } purchase.timestampFirstSuccessfulPay = now; @@ -721,7 +722,7 @@ async function storeFirstPaySuccess( if (protoAr) { const ar = Duration.fromTalerProtocolDuration(protoAr); logger.info("auto_refund present"); - purchase.purchaseStatus = PurchaseStatus.QueryingAutoRefund; + purchase.purchaseStatus = PurchaseStatus.PendingQueryingAutoRefund; purchase.autoRefundDeadline = AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration(AbsoluteTime.now(), ar), ); @@ -760,8 +761,8 @@ async function storePayReplaySuccess( } const oldTxState = computePayMerchantTransactionState(purchase); if ( - purchase.purchaseStatus === PurchaseStatus.Paying || - purchase.purchaseStatus === PurchaseStatus.PayingReplay + purchase.purchaseStatus === PurchaseStatus.PendingPaying || + purchase.purchaseStatus === PurchaseStatus.PendingPayingReplay ) { purchase.purchaseStatus = PurchaseStatus.Done; } @@ -1058,7 +1059,7 @@ export async function checkPaymentByProposalId( } const oldTxState = computePayMerchantTransactionState(p); p.lastSessionId = sessionId; - p.purchaseStatus = PurchaseStatus.PayingReplay; + p.purchaseStatus = PurchaseStatus.PendingPayingReplay; await tx.purchases.put(p); const newTxState = computePayMerchantTransactionState(p); return { oldTxState, newTxState }; @@ -1098,8 +1099,8 @@ export async function checkPaymentByProposalId( } else { const paid = purchase.purchaseStatus === PurchaseStatus.Done || - purchase.purchaseStatus === PurchaseStatus.QueryingRefund || - purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund; + purchase.purchaseStatus === PurchaseStatus.PendingQueryingRefund || + purchase.purchaseStatus === PurchaseStatus.PendingQueryingAutoRefund; const download = await expectProposalDownload(ws, purchase); return { status: PreparePayResultType.AlreadyConfirmed, @@ -1348,7 +1349,7 @@ export async function confirmPay( logger.trace(`changing session ID to ${sessionIdOverride}`); purchase.lastSessionId = sessionIdOverride; if (purchase.purchaseStatus === PurchaseStatus.Done) { - purchase.purchaseStatus = PurchaseStatus.PayingReplay; + purchase.purchaseStatus = PurchaseStatus.PendingPayingReplay; } await tx.purchases.put(purchase); } @@ -1424,7 +1425,7 @@ export async function confirmPay( }; p.lastSessionId = sessionId; p.timestampAccept = TalerPreciseTimestamp.now(); - p.purchaseStatus = PurchaseStatus.Paying; + p.purchaseStatus = PurchaseStatus.PendingPaying; await tx.purchases.put(p); await spendCoins(ws, tx, { //`txn:proposal:${p.proposalId}` @@ -1440,7 +1441,7 @@ export async function confirmPay( }); break; case PurchaseStatus.Done: - case PurchaseStatus.Paying: + case PurchaseStatus.PendingPaying: default: break; } @@ -1481,14 +1482,14 @@ export async function processPurchase( } switch (purchase.purchaseStatus) { - case PurchaseStatus.DownloadingProposal: + case PurchaseStatus.PendingDownloadingProposal: return processDownloadProposal(ws, proposalId); - case PurchaseStatus.Paying: - case PurchaseStatus.PayingReplay: + case PurchaseStatus.PendingPaying: + case PurchaseStatus.PendingPayingReplay: return processPurchasePay(ws, proposalId); - case PurchaseStatus.QueryingRefund: + case PurchaseStatus.PendingQueryingRefund: return processPurchaseQueryRefund(ws, purchase); - case PurchaseStatus.QueryingAutoRefund: + case PurchaseStatus.PendingQueryingAutoRefund: return processPurchaseAutoRefund(ws, purchase); case PurchaseStatus.AbortingWithRefund: return processPurchaseAbortingRefund(ws, purchase); @@ -1540,8 +1541,8 @@ export async function processPurchasePay( }; } switch (purchase.purchaseStatus) { - case PurchaseStatus.Paying: - case PurchaseStatus.PayingReplay: + case PurchaseStatus.PendingPaying: + case PurchaseStatus.PendingPayingReplay: break; default: return OperationAttemptResult.finishedEmpty(); @@ -1757,11 +1758,11 @@ export async function abortPayMerchant( logger.warn(`tried to abort successful payment`); return; } - if (oldStatus === PurchaseStatus.Paying) { + if (oldStatus === PurchaseStatus.PendingPaying) { purchase.purchaseStatus = PurchaseStatus.AbortingWithRefund; } await tx.purchases.put(purchase); - if (oldStatus === PurchaseStatus.Paying) { + if (oldStatus === PurchaseStatus.PendingPaying) { if (purchase.payInfo) { const coinSel = purchase.payInfo.payCoinSelection; const currency = Amounts.currencyOf(purchase.payInfo.totalPayCost); @@ -1789,32 +1790,146 @@ export async function abortPayMerchant( ws.workAvailable.trigger(); } + +const transitionSuspend: { [x in PurchaseStatus]?: { + next: PurchaseStatus | undefined, +} } = { + [PurchaseStatus.PendingDownloadingProposal]: { + next: PurchaseStatus.SuspendedDownloadingProposal, + }, + [PurchaseStatus.AbortingWithRefund]: { + next: PurchaseStatus.SuspendedAbortingWithRefund, + }, + [PurchaseStatus.PendingPaying]: { + next: PurchaseStatus.SuspendedPaying, + }, + [PurchaseStatus.PendingPayingReplay]: { + next: PurchaseStatus.SuspendedPayingReplay, + }, + [PurchaseStatus.PendingQueryingAutoRefund]: { + next: PurchaseStatus.SuspendedQueryingAutoRefund, + } +} + +const transitionResume: { [x in PurchaseStatus]?: { + next: PurchaseStatus | undefined, +} } = { + [PurchaseStatus.SuspendedDownloadingProposal]: { + next: PurchaseStatus.PendingDownloadingProposal, + }, + [PurchaseStatus.SuspendedAbortingWithRefund]: { + next: PurchaseStatus.AbortingWithRefund, + }, + [PurchaseStatus.SuspendedPaying]: { + next: PurchaseStatus.PendingPaying, + }, + [PurchaseStatus.SuspendedPayingReplay]: { + next: PurchaseStatus.PendingPayingReplay, + }, + [PurchaseStatus.SuspendedQueryingAutoRefund]: { + next: PurchaseStatus.PendingQueryingAutoRefund, + } +} + + +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 { switch (purchaseRecord.purchaseStatus) { // Pending States - case PurchaseStatus.DownloadingProposal: + case PurchaseStatus.PendingDownloadingProposal: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.ClaimProposal, }; - case PurchaseStatus.Paying: + case PurchaseStatus.PendingPaying: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.SubmitPayment, }; - case PurchaseStatus.PayingReplay: + case PurchaseStatus.PendingPayingReplay: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.RebindSession, }; - case PurchaseStatus.QueryingAutoRefund: + case PurchaseStatus.PendingQueryingAutoRefund: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.AutoRefund, }; - case PurchaseStatus.QueryingRefund: + case PurchaseStatus.PendingQueryingRefund: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.CheckRefund, @@ -1937,7 +2052,7 @@ async function processPurchaseAutoRefund( logger.warn("purchase does not exist anymore"); return; } - if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { + if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) { return; } const oldTxState = computePayMerchantTransactionState(p); @@ -1982,7 +2097,7 @@ async function processPurchaseAutoRefund( logger.warn("purchase does not exist anymore"); return; } - if (p.purchaseStatus !== PurchaseStatus.QueryingAutoRefund) { + if (p.purchaseStatus !== PurchaseStatus.PendingQueryingAutoRefund) { return; } const oldTxState = computePayMerchantTransactionState(p); @@ -2118,7 +2233,7 @@ async function processPurchaseQueryRefund( logger.warn("purchase does not exist anymore"); return undefined; } - if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { + if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) { return undefined; } const oldTxState = computePayMerchantTransactionState(p); @@ -2143,7 +2258,7 @@ async function processPurchaseQueryRefund( logger.warn("purchase does not exist anymore"); return; } - if (p.purchaseStatus !== PurchaseStatus.QueryingRefund) { + if (p.purchaseStatus !== PurchaseStatus.PendingQueryingRefund) { return; } const oldTxState = computePayMerchantTransactionState(p); @@ -2242,7 +2357,7 @@ export async function startQueryRefund( return; } const oldTxState = computePayMerchantTransactionState(p); - p.purchaseStatus = PurchaseStatus.QueryingRefund; + p.purchaseStatus = PurchaseStatus.PendingQueryingRefund; const newTxState = computePayMerchantTransactionState(p); await tx.purchases.put(p); return { oldTxState, newTxState }; diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index fb1260e3c..95878543b 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -1008,7 +1008,7 @@ export async function processPeerPushCredit( const amount = Amounts.parseOrThrow(contractTerms.amount); if ( - peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired && + peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired && peerInc.kycInfo ) { const txId = constructTransactionIdentifier({ @@ -1080,7 +1080,7 @@ export async function processPeerPushCredit( paytoHash: kycPending.h_payto, requirementRow: kycPending.requirement_row, }; - peerInc.status = PeerPushPaymentIncomingStatus.MergeKycRequired; + peerInc.status = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; await tx.peerPushPaymentIncoming.put(peerInc); }); return { @@ -1122,7 +1122,7 @@ export async function processPeerPushCredit( } if ( peerInc.status === PeerPushPaymentIncomingStatus.PendingMerge || - peerInc.status === PeerPushPaymentIncomingStatus.MergeKycRequired + peerInc.status === PeerPushPaymentIncomingStatus.PendingMergeKycRequired ) { peerInc.status = PeerPushPaymentIncomingStatus.Done; } @@ -2186,6 +2186,345 @@ export async function suspendPeerPushDebitTransaction( notifyTransition(ws, transactionId, transitionInfo); } +export async function suspendPeerPullDebitTransaction( + ws: InternalWalletState, + peerPullPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullDebit, + peerPullPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadWrite(async (tx) => { + const pullDebitRec = await tx.peerPullPaymentIncoming.get( + peerPullPaymentIncomingId, + ); + if (!pullDebitRec) { + logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPullDebitRecordStatus | undefined = undefined; + switch (pullDebitRec.status) { + case PeerPullDebitRecordStatus.DialogProposed: + break; + case PeerPullDebitRecordStatus.DonePaid: + break; + case PeerPullDebitRecordStatus.PendingDeposit: + newStatus = PeerPullDebitRecordStatus.SuspendedDeposit; + break; + case PeerPullDebitRecordStatus.SuspendedDeposit: + break; + default: + assertUnreachable(pullDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); + pullDebitRec.status = newStatus; + const newTxState = computePeerPullDebitTransactionState(pullDebitRec); + await tx.peerPullPaymentIncoming.put(pullDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPullDebitTransaction( + ws: InternalWalletState, + peerPullPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullDebit, + peerPullPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPullDebit, + peerPullPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPullPaymentIncoming]) + .runReadWrite(async (tx) => { + const pullDebitRec = await tx.peerPullPaymentIncoming.get( + peerPullPaymentIncomingId, + ); + if (!pullDebitRec) { + logger.warn(`peer pull debit ${peerPullPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPullDebitRecordStatus | undefined = undefined; + switch (pullDebitRec.status) { + case PeerPullDebitRecordStatus.DialogProposed: + case PeerPullDebitRecordStatus.DonePaid: + case PeerPullDebitRecordStatus.PendingDeposit: + break; + case PeerPullDebitRecordStatus.SuspendedDeposit: + newStatus = PeerPullDebitRecordStatus.PendingDeposit; + break; + default: + assertUnreachable(pullDebitRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullDebitTransactionState(pullDebitRec); + pullDebitRec.status = newStatus; + const newTxState = computePeerPullDebitTransactionState(pullDebitRec); + await tx.peerPullPaymentIncoming.put(pullDebitRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function suspendPeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const pushCreditRec = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushPaymentIncomingStatus.Done: + case PeerPushPaymentIncomingStatus.SuspendedMerge: + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + break; + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + newStatus = PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired; + break; + case PeerPushPaymentIncomingStatus.PendingMerge: + newStatus = PeerPushPaymentIncomingStatus.SuspendedMerge; + break; + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + // FIXME: Suspend internal withdrawal transaction! + newStatus = PeerPushPaymentIncomingStatus.SuspendedWithdrawing; + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushPaymentIncoming.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumePeerPushCreditTransaction( + ws: InternalWalletState, + peerPushPaymentIncomingId: string, +) { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushCredit, + peerPushPaymentIncomingId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.peerPushPaymentIncoming]) + .runReadWrite(async (tx) => { + const pushCreditRec = await tx.peerPushPaymentIncoming.get( + peerPushPaymentIncomingId, + ); + if (!pushCreditRec) { + logger.warn(`peer push credit ${peerPushPaymentIncomingId} not found`); + return; + } + let newStatus: PeerPushPaymentIncomingStatus | undefined = undefined; + switch (pushCreditRec.status) { + case PeerPushPaymentIncomingStatus.DialogProposed: + case PeerPushPaymentIncomingStatus.Done: + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: + case PeerPushPaymentIncomingStatus.PendingMerge: + case PeerPushPaymentIncomingStatus.PendingWithdrawing: + case PeerPushPaymentIncomingStatus.SuspendedMerge: + newStatus = PeerPushPaymentIncomingStatus.PendingMerge; + break; + case PeerPushPaymentIncomingStatus.SuspendedMergeKycRequired: + newStatus = PeerPushPaymentIncomingStatus.PendingMergeKycRequired; + break; + case PeerPushPaymentIncomingStatus.SuspendedWithdrawing: + // FIXME: resume underlying "internal-withdrawal" transaction. + newStatus = PeerPushPaymentIncomingStatus.PendingWithdrawing; + break; + default: + assertUnreachable(pushCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPushCreditTransactionState(pushCreditRec); + pushCreditRec.status = newStatus; + const newTxState = computePeerPushCreditTransactionState(pushCreditRec); + await tx.peerPushPaymentIncoming.put(pushCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +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.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + newStatus = PeerPullPaymentInitiationStatus.SuspendedCreatePurse; + break; + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + newStatus = PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired; + break; + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + newStatus = PeerPullPaymentInitiationStatus.SuspendedWithdrawing; + break; + case PeerPullPaymentInitiationStatus.PendingReady: + newStatus = PeerPullPaymentInitiationStatus.SuspendedReady; + break; + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + case PeerPullPaymentInitiationStatus.SuspendedReady: + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.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.peerPullPaymentInitiations]) + .runReadWrite(async (tx) => { + const pullCreditRec = await tx.peerPullPaymentInitiations.get(pursePub); + if (!pullCreditRec) { + logger.warn(`peer pull credit ${pursePub} not found`); + return; + } + let newStatus: PeerPullPaymentInitiationStatus | undefined = undefined; + switch (pullCreditRec.status) { + case PeerPullPaymentInitiationStatus.PendingCreatePurse: + case PeerPullPaymentInitiationStatus.PendingMergeKycRequired: + case PeerPullPaymentInitiationStatus.PendingWithdrawing: + case PeerPullPaymentInitiationStatus.PendingReady: + case PeerPullPaymentInitiationStatus.DonePurseDeposited: + case PeerPullPaymentInitiationStatus.SuspendedCreatePurse: + newStatus = PeerPullPaymentInitiationStatus.PendingCreatePurse; + break; + case PeerPullPaymentInitiationStatus.SuspendedMergeKycRequired: + newStatus = PeerPullPaymentInitiationStatus.PendingMergeKycRequired; + break; + case PeerPullPaymentInitiationStatus.SuspendedReady: + newStatus = PeerPullPaymentInitiationStatus.PendingReady; + break; + case PeerPullPaymentInitiationStatus.SuspendedWithdrawing: + newStatus = PeerPullPaymentInitiationStatus.PendingWithdrawing; + break; + default: + assertUnreachable(pullCreditRec.status); + } + if (newStatus != null) { + const oldTxState = computePeerPullCreditTransactionState(pullCreditRec); + pullCreditRec.status = newStatus; + const newTxState = computePeerPullCreditTransactionState(pullCreditRec); + await tx.peerPullPaymentInitiations.put(pullCreditRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + export async function resumePeerPushDebitTransaction( ws: InternalWalletState, pursePub: string, @@ -2244,6 +2583,7 @@ export async function resumePeerPushDebitTransaction( } return undefined; }); + ws.workAvailable.trigger(); notifyTransition(ws, transactionId, transitionInfo); } @@ -2265,7 +2605,7 @@ export function computePeerPushCreditTransactionState( return { major: TransactionMajorState.Done, }; - case PeerPushPaymentIncomingStatus.MergeKycRequired: + case PeerPushPaymentIncomingStatus.PendingMergeKycRequired: return { major: TransactionMajorState.Pending, minor: TransactionMinorState.KycRequired, diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 524970faa..70b595c2f 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -48,6 +48,7 @@ import { CoinSourceType, DenominationRecord, TipRecord, + TipRecordStatus, } from "../db.js"; import { makeErrorDetail } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../internal-wallet-state.js"; @@ -57,6 +58,7 @@ import { } from "@gnu-taler/taler-util/http"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { + constructTaskIdentifier, OperationAttemptResult, OperationAttemptResultType, } from "../util/retries.js"; @@ -68,7 +70,13 @@ import { updateWithdrawalDenoms, } from "./withdraw.js"; import { selectWithdrawalDenominations } from "../util/coinSelection.js"; -import { constructTransactionIdentifier } from "./transactions.js"; +import { + constructTransactionIdentifier, + notifyTransition, + stopLongpolling, +} from "./transactions.js"; +import { PendingTaskType } from "../pending-types.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; const logger = new Logger("operations/tip.ts"); @@ -156,6 +164,7 @@ export async function prepareTip( const newTipRecord: TipRecord = { walletTipId: walletTipId, acceptedTimestamp: undefined, + status: TipRecordStatus.PendingPickup, tipAmountRaw: Amounts.stringify(amount), tipExpiration: tipPickupStatus.expiration, exchangeBaseUrl: tipPickupStatus.exchange_url, @@ -180,7 +189,7 @@ export async function prepareTip( const transactionId = constructTransactionIdentifier({ tag: TransactionType.Tip, walletTipId: tipRecord.walletTipId, - }) + }); const tipStatus: PrepareTipResult = { accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, @@ -410,3 +419,98 @@ export async function acceptTip( next_url: found?.next_url, }; } + +export async function suspendTipTransaction( + ws: InternalWalletState, + walletTipId: string, +): Promise { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.TipPickup, + walletTipId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Tip, + walletTipId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.tips]) + .runReadWrite(async (tx) => { + const tipRec = await tx.tips.get(walletTipId); + if (!tipRec) { + logger.warn(`transaction tip ${walletTipId} not found`); + return; + } + let newStatus: TipRecordStatus | undefined = undefined; + switch (tipRec.status) { + case TipRecordStatus.Done: + case TipRecordStatus.SuspendidPickup: + break; + case TipRecordStatus.PendingPickup: + newStatus = TipRecordStatus.SuspendidPickup; + break; + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeTipTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeTipTransactionStatus(tipRec); + await tx.tips.put(tipRec); + return { + oldTxState, + newTxState, + }; + } + return undefined; + }); + ws.workAvailable.trigger(); + notifyTransition(ws, transactionId, transitionInfo); +} + +export async function resumeTipTransaction( + ws: InternalWalletState, + walletTipId: string, +): Promise { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.TipPickup, + walletTipId, + }); + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Tip, + walletTipId, + }); + stopLongpolling(ws, taskId); + const transitionInfo = await ws.db + .mktx((x) => [x.tips]) + .runReadWrite(async (tx) => { + const tipRec = await tx.tips.get(walletTipId); + if (!tipRec) { + logger.warn(`transaction tip ${walletTipId} not found`); + return; + } + let newStatus: TipRecordStatus | undefined = undefined; + switch (tipRec.status) { + case TipRecordStatus.Done: + case TipRecordStatus.SuspendidPickup: + newStatus = TipRecordStatus.PendingPickup; + break; + case TipRecordStatus.PendingPickup: + break; + default: + assertUnreachable(tipRec.status); + } + if (newStatus != null) { + const oldTxState = computeTipTransactionStatus(tipRec); + tipRec.status = newStatus; + const newTxState = computeTipTransactionStatus(tipRec); + await tx.tips.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 3645edd93..f1cfaed45 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -86,20 +86,40 @@ import { computeRefundTransactionState, expectProposalDownload, extractContractData, + resumePayMerchant, + suspendPayMerchant, } from "./pay-merchant.js"; import { computePeerPullCreditTransactionState, computePeerPullDebitTransactionState, computePeerPushCreditTransactionState, computePeerPushDebitTransactionState, + resumePeerPullCreditTransaction, + resumePeerPullDebitTransaction, + resumePeerPushCreditTransaction, + resumePeerPushDebitTransaction, + suspendPeerPullCreditTransaction, + suspendPeerPullDebitTransaction, + suspendPeerPushCreditTransaction, + suspendPeerPushDebitTransaction, } from "./pay-peer.js"; -import { computeRefreshTransactionState } from "./refresh.js"; -import { computeTipTransactionStatus } from "./tip.js"; +import { + computeRefreshTransactionState, + resumeRefreshGroup, + suspendRefreshGroup, +} from "./refresh.js"; +import { + computeTipTransactionStatus, + resumeTipTransaction, + suspendTipTransaction, +} from "./tip.js"; import { abortWithdrawalTransaction, augmentPaytoUrisForWithdrawal, cancelAbortingWithdrawalTransaction, computeWithdrawalTransactionStatus, + resumeWithdrawalTransaction, + suspendWithdrawalTransaction, } from "./withdraw.js"; const logger = new Logger("taler-wallet-core:transactions.ts"); @@ -159,6 +179,7 @@ export async function getTransactionById( } switch (parsedTx.tag) { + case TransactionType.InternalWithdrawal: case TransactionType.Withdrawal: { const withdrawalGroupId = parsedTx.withdrawalGroupId; return await ws.db @@ -844,7 +865,7 @@ async function buildTransactionForPurchase( proposalId: purchaseRecord.proposalId, info, refundQueryActive: - purchaseRecord.purchaseStatus === PurchaseStatus.QueryingRefund, + purchaseRecord.purchaseStatus === PurchaseStatus.PendingQueryingRefund, ...(ort?.lastError ? { error: ort.lastError } : {}), }; } @@ -1197,7 +1218,8 @@ export type ParsedTransactionIdentifier = | { tag: TransactionType.Refresh; refreshGroupId: string } | { tag: TransactionType.Refund; refundGroupId: string } | { tag: TransactionType.Tip; walletTipId: string } - | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }; + | { tag: TransactionType.Withdrawal; withdrawalGroupId: string } + | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }; export function constructTransactionIdentifier( pTxId: ParsedTransactionIdentifier, @@ -1223,6 +1245,8 @@ export function constructTransactionIdentifier( return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr; case TransactionType.Withdrawal: return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; + case TransactionType.InternalWithdrawal: + return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; default: assertUnreachable(pTxId); } @@ -1242,6 +1266,10 @@ export function parseTransactionIdentifier( const [prefix, type, ...rest] = txnParts; + if (prefix != "txn") { + throw Error("invalid transaction identifier"); + } + switch (type) { case TransactionType.Deposit: return { tag: TransactionType.Deposit, depositGroupId: rest[0] }; @@ -1329,6 +1357,7 @@ export async function retryTransaction( stopLongpolling(ws, taskId); break; } + case TransactionType.InternalWithdrawal: case TransactionType.Withdrawal: { // FIXME: Abort current long-poller! const taskId = constructTaskIdentifier({ @@ -1366,8 +1395,38 @@ export async function retryTransaction( stopLongpolling(ws, taskId); break; } - default: + case TransactionType.PeerPullDebit: { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPullDebit, + peerPullPaymentIncomingId: parsedTx.peerPullPaymentIncomingId, + }); + await resetOperationTimeout(ws, taskId); + stopLongpolling(ws, taskId); + break; + } + case TransactionType.PeerPushCredit: { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushCredit, + peerPushPaymentIncomingId: parsedTx.peerPushPaymentIncomingId, + }); + await resetOperationTimeout(ws, taskId); + stopLongpolling(ws, taskId); break; + } + case TransactionType.PeerPushDebit: { + const taskId = constructTaskIdentifier({ + tag: PendingTaskType.PeerPushDebit, + pursePub: parsedTx.pursePub, + }); + await resetOperationTimeout(ws, taskId); + stopLongpolling(ws, taskId); + break; + } + case TransactionType.Refund: + // Nothing to do for a refund transaction. + break; + default: + assertUnreachable(parsedTx); } } @@ -1389,8 +1448,35 @@ export async function suspendTransaction( case TransactionType.Deposit: await suspendDepositGroup(ws, tx.depositGroupId); return; + case TransactionType.Refresh: + await suspendRefreshGroup(ws, tx.refreshGroupId); + return; + case TransactionType.InternalWithdrawal: + case TransactionType.Withdrawal: + await suspendWithdrawalTransaction(ws, tx.withdrawalGroupId); + return; + case TransactionType.Payment: + await suspendPayMerchant(ws, tx.proposalId); + return; + case TransactionType.PeerPullCredit: + await suspendPeerPullCreditTransaction(ws, tx.pursePub); + break; + case TransactionType.PeerPushDebit: + await suspendPeerPushDebitTransaction(ws, tx.pursePub); + break; + case TransactionType.PeerPullDebit: + await suspendPeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); + break; + case TransactionType.PeerPushCredit: + await suspendPeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); + break; + case TransactionType.Refund: + throw Error("refund transactions can't be suspended or resumed"); + case TransactionType.Tip: + await suspendTipTransaction(ws, tx.walletTipId); + break; default: - logger.warn(`unable to suspend transaction of type '${tx.tag}'`); + assertUnreachable(tx); } } @@ -1429,8 +1515,33 @@ export async function resumeTransaction( case TransactionType.Deposit: await resumeDepositGroup(ws, tx.depositGroupId); return; - default: - logger.warn(`unable to resume transaction of type '${tx.tag}'`); + 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: + await resumePeerPullDebitTransaction(ws, tx.peerPullPaymentIncomingId); + break; + case TransactionType.PeerPushCredit: + await resumePeerPushCreditTransaction(ws, tx.peerPushPaymentIncomingId); + break; + case TransactionType.Refund: + throw Error("refund transactions can't be suspended or resumed"); + case TransactionType.Tip: + await resumeTipTransaction(ws, tx.walletTipId); + break; } } diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index ae170fa2c..7636395bd 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -259,7 +259,7 @@ export async function resumeWithdrawalTransaction( } return undefined; }); - + ws.workAvailable.trigger(); const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId, -- cgit v1.2.3