wallet-core: add missing resume/suspend implementations

This commit is contained in:
Florian Dold 2023-05-30 09:33:32 +02:00
parent 246f914ca6
commit 0323067c07
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 755 additions and 58 deletions

View File

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

View File

@ -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:

View File

@ -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<void> {
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<void> {
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 };

View File

@ -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,

View File

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

View File

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

View File

@ -259,7 +259,7 @@ export async function resumeWithdrawalTransaction(
}
return undefined;
});
ws.workAvailable.trigger();
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,