aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-06-02 15:53:46 +0200
committerFlorian Dold <florian@dold.me>2023-06-02 15:53:46 +0200
commit1ee9ef80bddcc91a8e542ffc44cd23e056e751d4 (patch)
treebc4f20b4adc699076c9efdaac8fd5791886c7a03 /packages
parent2f4f43cc1f5acb37fce00318db1507b407332324 (diff)
wallet-core: fix tipping state machine issues
Diffstat (limited to 'packages')
-rw-r--r--packages/taler-wallet-core/src/db.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts29
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts96
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<string, never> = {},
): Promise<OperationAttemptResult> {
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<AcceptTipResponse> {
- 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);
}