wallet-core: fix tipping state machine issues

This commit is contained in:
Florian Dold 2023-06-02 15:53:46 +02:00
parent 2f4f43cc1f
commit 1ee9ef80bd
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 82 additions and 47 deletions

View File

@ -876,7 +876,9 @@ export interface TipRecord {
export enum TipRecordStatus {
PendingPickup = 10,
SuspendidPickup = 21,
SuspendidPickup = 20,
DialogAccept = 30,
Done = 50,
Aborted = 51,

View File

@ -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);
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 (differentPurchase) {
if (otherPurchase) {
logger.warn("repurchase detected");
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
p.repurchaseProposalId = differentPurchase.proposalId;
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;
}

View File

@ -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");
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);
}