wallet-core: fix tipping state machine issues
This commit is contained in:
parent
2f4f43cc1f
commit
1ee9ef80bd
@ -876,7 +876,9 @@ export interface TipRecord {
|
|||||||
export enum TipRecordStatus {
|
export enum TipRecordStatus {
|
||||||
PendingPickup = 10,
|
PendingPickup = 10,
|
||||||
|
|
||||||
SuspendidPickup = 21,
|
SuspendidPickup = 20,
|
||||||
|
|
||||||
|
DialogAccept = 30,
|
||||||
|
|
||||||
Done = 50,
|
Done = 50,
|
||||||
Aborted = 51,
|
Aborted = 51,
|
||||||
|
@ -532,21 +532,23 @@ async function processDownloadProposal(
|
|||||||
h: contractTermsHash,
|
h: contractTermsHash,
|
||||||
contractTermsRaw: proposalResp.contract_terms,
|
contractTermsRaw: proposalResp.contract_terms,
|
||||||
});
|
});
|
||||||
if (
|
const isResourceFulfillmentUrl =
|
||||||
fulfillmentUrl &&
|
fulfillmentUrl &&
|
||||||
(fulfillmentUrl.startsWith("http://") ||
|
(fulfillmentUrl.startsWith("http://") ||
|
||||||
fulfillmentUrl.startsWith("https://"))
|
fulfillmentUrl.startsWith("https://"));
|
||||||
) {
|
let otherPurchase: PurchaseRecord | undefined;
|
||||||
const differentPurchase =
|
if (isResourceFulfillmentUrl) {
|
||||||
await tx.purchases.indexes.byFulfillmentUrl.get(fulfillmentUrl);
|
otherPurchase = await tx.purchases.indexes.byFulfillmentUrl.get(
|
||||||
// FIXME: Adjust this to account for refunds, don't count as repurchase
|
fulfillmentUrl,
|
||||||
// if original order is refunded.
|
);
|
||||||
if (differentPurchase) {
|
}
|
||||||
logger.warn("repurchase detected");
|
// FIXME: Adjust this to account for refunds, don't count as repurchase
|
||||||
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
// if original order is refunded.
|
||||||
p.repurchaseProposalId = differentPurchase.proposalId;
|
if (otherPurchase) {
|
||||||
await tx.purchases.put(p);
|
logger.warn("repurchase detected");
|
||||||
}
|
p.purchaseStatus = PurchaseStatus.RepurchaseDetected;
|
||||||
|
p.repurchaseProposalId = otherPurchase.proposalId;
|
||||||
|
await tx.purchases.put(p);
|
||||||
} else {
|
} else {
|
||||||
p.purchaseStatus = PurchaseStatus.DialogProposed;
|
p.purchaseStatus = PurchaseStatus.DialogProposed;
|
||||||
await tx.purchases.put(p);
|
await tx.purchases.put(p);
|
||||||
@ -602,6 +604,7 @@ async function createPurchase(
|
|||||||
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
(!noncePriv || oldProposal.noncePriv === noncePriv) &&
|
||||||
oldProposal.claimToken === claimToken
|
oldProposal.claimToken === claimToken
|
||||||
) {
|
) {
|
||||||
|
// FIXME: This lacks proper error handling
|
||||||
await processDownloadProposal(ws, oldProposal.proposalId);
|
await processDownloadProposal(ws, oldProposal.proposalId);
|
||||||
return oldProposal.proposalId;
|
return oldProposal.proposalId;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,6 @@ import {
|
|||||||
PrepareTipResult,
|
PrepareTipResult,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerPreciseTimestamp,
|
TalerPreciseTimestamp,
|
||||||
TalerProtocolTimestamp,
|
|
||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
TransactionAction,
|
TransactionAction,
|
||||||
TransactionMajorState,
|
TransactionMajorState,
|
||||||
@ -102,17 +101,21 @@ export function computeTipTransactionStatus(
|
|||||||
major: TransactionMajorState.Pending,
|
major: TransactionMajorState.Pending,
|
||||||
minor: TransactionMinorState.Pickup,
|
minor: TransactionMinorState.Pickup,
|
||||||
};
|
};
|
||||||
|
case TipRecordStatus.DialogAccept:
|
||||||
|
return {
|
||||||
|
major: TransactionMajorState.Dialog,
|
||||||
|
minor: TransactionMinorState.Proposed,
|
||||||
|
};
|
||||||
case TipRecordStatus.SuspendidPickup:
|
case TipRecordStatus.SuspendidPickup:
|
||||||
return {
|
return {
|
||||||
major: TransactionMajorState.Pending,
|
major: TransactionMajorState.Pending,
|
||||||
minor: TransactionMinorState.User,
|
minor: TransactionMinorState.Pickup,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
assertUnreachable(tipRecord.status);
|
assertUnreachable(tipRecord.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function computeTipTransactionActions(
|
export function computeTipTransactionActions(
|
||||||
tipRecord: TipRecord,
|
tipRecord: TipRecord,
|
||||||
): TransactionAction[] {
|
): TransactionAction[] {
|
||||||
@ -125,6 +128,8 @@ export function computeTipTransactionActions(
|
|||||||
return [TransactionAction.Suspend, TransactionAction.Fail];
|
return [TransactionAction.Suspend, TransactionAction.Fail];
|
||||||
case TipRecordStatus.SuspendidPickup:
|
case TipRecordStatus.SuspendidPickup:
|
||||||
return [TransactionAction.Resume, TransactionAction.Fail];
|
return [TransactionAction.Resume, TransactionAction.Fail];
|
||||||
|
case TipRecordStatus.DialogAccept:
|
||||||
|
return [TransactionAction.Abort];
|
||||||
default:
|
default:
|
||||||
assertUnreachable(tipRecord.status);
|
assertUnreachable(tipRecord.status);
|
||||||
}
|
}
|
||||||
@ -190,7 +195,7 @@ export async function prepareTip(
|
|||||||
const newTipRecord: TipRecord = {
|
const newTipRecord: TipRecord = {
|
||||||
walletTipId: walletTipId,
|
walletTipId: walletTipId,
|
||||||
acceptedTimestamp: undefined,
|
acceptedTimestamp: undefined,
|
||||||
status: TipRecordStatus.PendingPickup,
|
status: TipRecordStatus.DialogAccept,
|
||||||
tipAmountRaw: Amounts.stringify(amount),
|
tipAmountRaw: Amounts.stringify(amount),
|
||||||
tipExpiration: tipPickupStatus.expiration,
|
tipExpiration: tipPickupStatus.expiration,
|
||||||
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
exchangeBaseUrl: tipPickupStatus.exchange_url,
|
||||||
@ -234,7 +239,6 @@ export async function prepareTip(
|
|||||||
export async function processTip(
|
export async function processTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
walletTipId: string,
|
walletTipId: string,
|
||||||
options: Record<string, never> = {},
|
|
||||||
): Promise<OperationAttemptResult> {
|
): Promise<OperationAttemptResult> {
|
||||||
const tipRecord = await ws.db
|
const tipRecord = await ws.db
|
||||||
.mktx((x) => [x.tips])
|
.mktx((x) => [x.tips])
|
||||||
@ -248,14 +252,22 @@ export async function processTip(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tipRecord.pickedUpTimestamp) {
|
switch (tipRecord.status) {
|
||||||
logger.warn("tip already picked up");
|
case TipRecordStatus.Aborted:
|
||||||
return {
|
case TipRecordStatus.DialogAccept:
|
||||||
type: OperationAttemptResultType.Finished,
|
case TipRecordStatus.Done:
|
||||||
result: undefined,
|
case TipRecordStatus.SuspendidPickup:
|
||||||
};
|
return {
|
||||||
|
type: OperationAttemptResultType.Finished,
|
||||||
|
result: undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Tip,
|
||||||
|
walletTipId,
|
||||||
|
});
|
||||||
|
|
||||||
const denomsForWithdraw = tipRecord.denomsSel;
|
const denomsForWithdraw = tipRecord.denomsSel;
|
||||||
|
|
||||||
const planchets: DerivedTipPlanchet[] = [];
|
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])
|
.mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const tr = await tx.tips.get(walletTipId);
|
const tr = await tx.tips.get(walletTipId);
|
||||||
if (!tr) {
|
if (!tr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (tr.pickedUpTimestamp) {
|
if (tr.status !== TipRecordStatus.PendingPickup) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldTxState = computeTipTransactionStatus(tr);
|
||||||
tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
|
tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
|
||||||
|
tr.status = TipRecordStatus.Done;
|
||||||
await tx.tips.put(tr);
|
await tx.tips.put(tr);
|
||||||
|
const newTxState = computeTipTransactionStatus(tr);
|
||||||
for (const cr of newCoinRecords) {
|
for (const cr of newCoinRecords) {
|
||||||
await makeCoinAvailable(ws, tx, cr);
|
await makeCoinAvailable(ws, tx, cr);
|
||||||
}
|
}
|
||||||
|
return { oldTxState, newTxState };
|
||||||
});
|
});
|
||||||
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Finished,
|
type: OperationAttemptResultType.Finished,
|
||||||
@ -416,33 +433,46 @@ export async function processTip(
|
|||||||
|
|
||||||
export async function acceptTip(
|
export async function acceptTip(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tipId: string,
|
walletTipId: string,
|
||||||
): Promise<AcceptTipResponse> {
|
): Promise<AcceptTipResponse> {
|
||||||
const found = await ws.db
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Tip,
|
||||||
|
walletTipId,
|
||||||
|
});
|
||||||
|
const dbRes = await ws.db
|
||||||
.mktx((x) => [x.tips])
|
.mktx((x) => [x.tips])
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const tipRecord = await tx.tips.get(tipId);
|
const tipRecord = await tx.tips.get(walletTipId);
|
||||||
if (!tipRecord) {
|
if (!tipRecord) {
|
||||||
logger.error("tip not found");
|
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.acceptedTimestamp = TalerPreciseTimestamp.now();
|
||||||
|
tipRecord.status = TipRecordStatus.PendingPickup;
|
||||||
await tx.tips.put(tipRecord);
|
await tx.tips.put(tipRecord);
|
||||||
|
const newTxState = computeTipTransactionStatus(tipRecord);
|
||||||
return tipRecord;
|
return { tipRecord, transitionInfo: { oldTxState, newTxState } };
|
||||||
});
|
});
|
||||||
|
|
||||||
if (found) {
|
if (!dbRes) {
|
||||||
await processTip(ws, tipId);
|
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 {
|
return {
|
||||||
transactionId: constructTransactionIdentifier({
|
transactionId: constructTransactionIdentifier({
|
||||||
tag: TransactionType.Tip,
|
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.Done:
|
||||||
case TipRecordStatus.SuspendidPickup:
|
case TipRecordStatus.SuspendidPickup:
|
||||||
case TipRecordStatus.Aborted:
|
case TipRecordStatus.Aborted:
|
||||||
|
case TipRecordStatus.DialogAccept:
|
||||||
break;
|
break;
|
||||||
case TipRecordStatus.PendingPickup:
|
case TipRecordStatus.PendingPickup:
|
||||||
newStatus = TipRecordStatus.SuspendidPickup;
|
newStatus = TipRecordStatus.SuspendidPickup;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertUnreachable(tipRec.status);
|
assertUnreachable(tipRec.status);
|
||||||
}
|
}
|
||||||
@ -519,14 +551,13 @@ export async function resumeTipTransaction(
|
|||||||
let newStatus: TipRecordStatus | undefined = undefined;
|
let newStatus: TipRecordStatus | undefined = undefined;
|
||||||
switch (tipRec.status) {
|
switch (tipRec.status) {
|
||||||
case TipRecordStatus.Done:
|
case TipRecordStatus.Done:
|
||||||
|
case TipRecordStatus.PendingPickup:
|
||||||
|
case TipRecordStatus.Aborted:
|
||||||
|
case TipRecordStatus.DialogAccept:
|
||||||
break;
|
break;
|
||||||
case TipRecordStatus.SuspendidPickup:
|
case TipRecordStatus.SuspendidPickup:
|
||||||
newStatus = TipRecordStatus.PendingPickup;
|
newStatus = TipRecordStatus.PendingPickup;
|
||||||
break;
|
break;
|
||||||
case TipRecordStatus.PendingPickup:
|
|
||||||
break;
|
|
||||||
case TipRecordStatus.Aborted:
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
assertUnreachable(tipRec.status);
|
assertUnreachable(tipRec.status);
|
||||||
}
|
}
|
||||||
@ -577,14 +608,13 @@ export async function abortTipTransaction(
|
|||||||
let newStatus: TipRecordStatus | undefined = undefined;
|
let newStatus: TipRecordStatus | undefined = undefined;
|
||||||
switch (tipRec.status) {
|
switch (tipRec.status) {
|
||||||
case TipRecordStatus.Done:
|
case TipRecordStatus.Done:
|
||||||
|
case TipRecordStatus.Aborted:
|
||||||
|
case TipRecordStatus.PendingPickup:
|
||||||
|
case TipRecordStatus.DialogAccept:
|
||||||
break;
|
break;
|
||||||
case TipRecordStatus.SuspendidPickup:
|
case TipRecordStatus.SuspendidPickup:
|
||||||
newStatus = TipRecordStatus.Aborted;
|
newStatus = TipRecordStatus.Aborted;
|
||||||
break;
|
break;
|
||||||
case TipRecordStatus.PendingPickup:
|
|
||||||
break;
|
|
||||||
case TipRecordStatus.Aborted:
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
assertUnreachable(tipRec.status);
|
assertUnreachable(tipRec.status);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user