From 1ee9ef80bddcc91a8e542ffc44cd23e056e751d4 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 2 Jun 2023 15:53:46 +0200 Subject: [PATCH] wallet-core: fix tipping state machine issues --- packages/taler-wallet-core/src/db.ts | 4 +- .../src/operations/pay-merchant.ts | 29 +++--- .../taler-wallet-core/src/operations/tip.ts | 96 ++++++++++++------- 3 files changed, 82 insertions(+), 47 deletions(-) diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 74332de33..afc7e2bf8 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -876,7 +876,9 @@ export interface TipRecord { export enum TipRecordStatus { PendingPickup = 10, - SuspendidPickup = 21, + SuspendidPickup = 20, + + DialogAccept = 30, Done = 50, Aborted = 51, diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index dce2a30ed..0097f5bcc 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -532,21 +532,23 @@ async function processDownloadProposal( h: contractTermsHash, contractTermsRaw: proposalResp.contract_terms, }); - if ( + const isResourceFulfillmentUrl = fulfillmentUrl && (fulfillmentUrl.startsWith("http://") || - fulfillmentUrl.startsWith("https://")) - ) { - const differentPurchase = - await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl); - // FIXME: Adjust this to account for refunds, don't count as repurchase - // if original order is refunded. - if (differentPurchase) { - logger.warn("repurchase detected"); - p.purchaseStatus = PurchaseStatus.RepurchaseDetected; - p.repurchaseProposalId = differentPurchase.proposalId; - await tx.purchases.put(p); - } + fulfillmentUrl.startsWith("https://")); + let otherPurchase: PurchaseRecord | undefined; + if (isResourceFulfillmentUrl) { + otherPurchase = await tx.purchases.indexes.byFulfillmentUrl.get( + fulfillmentUrl, + ); + } + // FIXME: Adjust this to account for refunds, don't count as repurchase + // if original order is refunded. + if (otherPurchase) { + logger.warn("repurchase detected"); + p.purchaseStatus = PurchaseStatus.RepurchaseDetected; + p.repurchaseProposalId = otherPurchase.proposalId; + await tx.purchases.put(p); } else { p.purchaseStatus = PurchaseStatus.DialogProposed; await tx.purchases.put(p); @@ -602,6 +604,7 @@ async function createPurchase( (!noncePriv || oldProposal.noncePriv === noncePriv) && oldProposal.claimToken === claimToken ) { + // FIXME: This lacks proper error handling await processDownloadProposal(ws, oldProposal.proposalId); return oldProposal.proposalId; } diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts index 02c933cba..1a565e02f 100644 --- a/packages/taler-wallet-core/src/operations/tip.ts +++ b/packages/taler-wallet-core/src/operations/tip.ts @@ -34,7 +34,6 @@ import { PrepareTipResult, TalerErrorCode, TalerPreciseTimestamp, - TalerProtocolTimestamp, TipPlanchetDetail, TransactionAction, TransactionMajorState, @@ -102,17 +101,21 @@ export function computeTipTransactionStatus( major: TransactionMajorState.Pending, minor: TransactionMinorState.Pickup, }; + case TipRecordStatus.DialogAccept: + return { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }; case TipRecordStatus.SuspendidPickup: return { major: TransactionMajorState.Pending, - minor: TransactionMinorState.User, + minor: TransactionMinorState.Pickup, }; default: assertUnreachable(tipRecord.status); } } - export function computeTipTransactionActions( tipRecord: TipRecord, ): TransactionAction[] { @@ -125,6 +128,8 @@ export function computeTipTransactionActions( return [TransactionAction.Suspend, TransactionAction.Fail]; case TipRecordStatus.SuspendidPickup: return [TransactionAction.Resume, TransactionAction.Fail]; + case TipRecordStatus.DialogAccept: + return [TransactionAction.Abort]; default: assertUnreachable(tipRecord.status); } @@ -190,7 +195,7 @@ export async function prepareTip( const newTipRecord: TipRecord = { walletTipId: walletTipId, acceptedTimestamp: undefined, - status: TipRecordStatus.PendingPickup, + status: TipRecordStatus.DialogAccept, tipAmountRaw: Amounts.stringify(amount), tipExpiration: tipPickupStatus.expiration, exchangeBaseUrl: tipPickupStatus.exchange_url, @@ -234,7 +239,6 @@ export async function prepareTip( export async function processTip( ws: InternalWalletState, walletTipId: string, - options: Record = {}, ): Promise { const tipRecord = await ws.db .mktx((x) => [x.tips]) @@ -248,14 +252,22 @@ export async function processTip( }; } - if (tipRecord.pickedUpTimestamp) { - logger.warn("tip already picked up"); - return { - type: OperationAttemptResultType.Finished, - result: undefined, - }; + switch (tipRecord.status) { + case TipRecordStatus.Aborted: + case TipRecordStatus.DialogAccept: + case TipRecordStatus.Done: + case TipRecordStatus.SuspendidPickup: + return { + type: OperationAttemptResultType.Finished, + result: undefined, + }; } + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Tip, + walletTipId, + }); + const denomsForWithdraw = tipRecord.denomsSel; const planchets: DerivedTipPlanchet[] = []; @@ -391,22 +403,27 @@ export async function processTip( }); } - await ws.db + const transitionInfo = await ws.db .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips]) .runReadWrite(async (tx) => { const tr = await tx.tips.get(walletTipId); if (!tr) { return; } - if (tr.pickedUpTimestamp) { + if (tr.status !== TipRecordStatus.PendingPickup) { return; } + const oldTxState = computeTipTransactionStatus(tr); tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); + tr.status = TipRecordStatus.Done; await tx.tips.put(tr); + const newTxState = computeTipTransactionStatus(tr); for (const cr of newCoinRecords) { await makeCoinAvailable(ws, tx, cr); } + return { oldTxState, newTxState }; }); + notifyTransition(ws, transactionId, transitionInfo); return { type: OperationAttemptResultType.Finished, @@ -416,33 +433,46 @@ export async function processTip( export async function acceptTip( ws: InternalWalletState, - tipId: string, + walletTipId: string, ): Promise { - const found = await ws.db + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.Tip, + walletTipId, + }); + const dbRes = await ws.db .mktx((x) => [x.tips]) .runReadWrite(async (tx) => { - const tipRecord = await tx.tips.get(tipId); + const tipRecord = await tx.tips.get(walletTipId); if (!tipRecord) { logger.error("tip not found"); - return undefined; + return; } + if (tipRecord.status != TipRecordStatus.DialogAccept) { + logger.warn("Unable to accept tip in the current state"); + return { tipRecord }; + } + const oldTxState = computeTipTransactionStatus(tipRecord); tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); + tipRecord.status = TipRecordStatus.PendingPickup; await tx.tips.put(tipRecord); - - return tipRecord; + const newTxState = computeTipTransactionStatus(tipRecord); + return { tipRecord, transitionInfo: { oldTxState, newTxState } }; }); - if (found) { - await processTip(ws, tipId); + if (!dbRes) { + throw Error("tip not found"); } - //FIXME: if tip is not found the behavior of the function is the same - // as the tip was found and finished + + notifyTransition(ws, transactionId, dbRes.transitionInfo); + + const tipRecord = dbRes.tipRecord; + return { transactionId: constructTransactionIdentifier({ tag: TransactionType.Tip, - walletTipId: tipId, + walletTipId: walletTipId, }), - next_url: found?.next_url, + next_url: tipRecord.next_url, }; } @@ -472,10 +502,12 @@ export async function suspendTipTransaction( case TipRecordStatus.Done: case TipRecordStatus.SuspendidPickup: case TipRecordStatus.Aborted: + case TipRecordStatus.DialogAccept: break; case TipRecordStatus.PendingPickup: newStatus = TipRecordStatus.SuspendidPickup; break; + default: assertUnreachable(tipRec.status); } @@ -519,14 +551,13 @@ export async function resumeTipTransaction( let newStatus: TipRecordStatus | undefined = undefined; switch (tipRec.status) { case TipRecordStatus.Done: + case TipRecordStatus.PendingPickup: + case TipRecordStatus.Aborted: + case TipRecordStatus.DialogAccept: break; case TipRecordStatus.SuspendidPickup: newStatus = TipRecordStatus.PendingPickup; break; - case TipRecordStatus.PendingPickup: - break; - case TipRecordStatus.Aborted: - break; default: assertUnreachable(tipRec.status); } @@ -577,14 +608,13 @@ export async function abortTipTransaction( let newStatus: TipRecordStatus | undefined = undefined; switch (tipRec.status) { case TipRecordStatus.Done: + case TipRecordStatus.Aborted: + case TipRecordStatus.PendingPickup: + case TipRecordStatus.DialogAccept: break; case TipRecordStatus.SuspendidPickup: newStatus = TipRecordStatus.Aborted; break; - case TipRecordStatus.PendingPickup: - break; - case TipRecordStatus.Aborted: - break; default: assertUnreachable(tipRec.status); }