summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/withdraw.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/operations/withdraw.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts233
1 files changed, 90 insertions, 143 deletions
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 86f05478a..4a50e0775 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019-2021 Taler Systems SA
+ (C) 2019-2024 Taler Systems SA
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -52,6 +52,7 @@ import {
TalerPreciseTimestamp,
TalerProtocolTimestamp,
TransactionAction,
+ TransactionIdStr,
TransactionMajorState,
TransactionMinorState,
TransactionState,
@@ -67,7 +68,6 @@ import {
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeWithdrawBatchResponse,
- codecForIntegrationBankConfig,
codecForReserveStatus,
codecForWalletKycUuid,
codecForWithdrawOperationStatusResponse,
@@ -103,7 +103,6 @@ import {
import { isWithdrawableDenom, timestampPreciseToDb } from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
- TaskIdentifiers,
TaskRunResult,
TaskRunResultType,
TombstoneTag,
@@ -111,9 +110,8 @@ import {
constructTaskIdentifier,
makeCoinAvailable,
makeCoinsVisible,
- runLongpollAsync,
} from "../operations/common.js";
-import { PendingTaskType } from "../pending-types.js";
+import { PendingTaskType, TaskId } from "../pending-types.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
import {
selectForcedWithdrawalDenominations,
@@ -141,7 +139,6 @@ import {
TransitionInfo,
constructTransactionIdentifier,
notifyTransition,
- stopLongpolling,
} from "./transactions.js";
/**
@@ -150,8 +147,8 @@ import {
const logger = new Logger("operations/withdraw.ts");
export class WithdrawTransactionContext implements TransactionContext {
- public transactionId: string;
- public retryTag: string;
+ readonly transactionId: TransactionIdStr;
+ readonly taskId: TaskId;
constructor(
public ws: InternalWalletState,
@@ -161,7 +158,7 @@ export class WithdrawTransactionContext implements TransactionContext {
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
- this.retryTag = constructTaskIdentifier({
+ this.taskId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
@@ -185,8 +182,7 @@ export class WithdrawTransactionContext implements TransactionContext {
}
async suspendTransaction(): Promise<void> {
- const { ws, withdrawalGroupId, transactionId, retryTag } = this;
- stopLongpolling(ws, retryTag);
+ const { ws, withdrawalGroupId, transactionId, taskId } = this;
const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
@@ -235,13 +231,12 @@ export class WithdrawTransactionContext implements TransactionContext {
}
return undefined;
});
-
+ ws.taskScheduler.stopShepherdTask(taskId);
notifyTransition(ws, transactionId, transitionInfo);
}
async abortTransaction(): Promise<void> {
- const { ws, withdrawalGroupId, transactionId } = this;
- stopLongpolling(ws, this.retryTag);
+ const { ws, withdrawalGroupId, transactionId, taskId } = this;
const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
@@ -297,12 +292,13 @@ export class WithdrawTransactionContext implements TransactionContext {
}
return undefined;
});
- ws.workAvailable.trigger();
+ ws.taskScheduler.stopShepherdTask(taskId);
notifyTransition(ws, transactionId, transitionInfo);
+ ws.taskScheduler.startShepherdTask(taskId);
}
async resumeTransaction(): Promise<void> {
- const { ws, withdrawalGroupId, transactionId } = this;
+ const { ws, withdrawalGroupId, transactionId, taskId: retryTag } = this;
const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
@@ -351,13 +347,12 @@ export class WithdrawTransactionContext implements TransactionContext {
}
return undefined;
});
- ws.workAvailable.trigger();
notifyTransition(ws, transactionId, transitionInfo);
+ ws.taskScheduler.startShepherdTask(retryTag);
}
async failTransaction(): Promise<void> {
- const { ws, withdrawalGroupId, transactionId, retryTag } = this;
- stopLongpolling(ws, retryTag);
+ const { ws, withdrawalGroupId, transactionId, taskId: retryTag } = this;
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
@@ -387,7 +382,9 @@ export class WithdrawTransactionContext implements TransactionContext {
}
return undefined;
});
+ ws.taskScheduler.stopShepherdTask(retryTag);
notifyTransition(ws, transactionId, stateUpdate);
+ ws.taskScheduler.startShepherdTask(retryTag);
}
}
@@ -744,10 +741,8 @@ async function transitionKycUrlUpdate(
kycUrl: string,
): Promise<void> {
let notificationKycUrl: string | undefined = undefined;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
+ const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
+ const transactionId = ctx.transactionId;
const transitionInfo = await ws.db
.mktx((x) => [x.planchets, x.withdrawalGroups])
@@ -782,7 +777,7 @@ async function transitionKycUrlUpdate(
experimentalUserData: notificationKycUrl,
});
}
- ws.workAvailable.trigger();
+ ws.taskScheduler.startShepherdTask(ctx.taskId);
}
async function handleKycRequired(
@@ -1273,7 +1268,7 @@ async function queryReserve(
ws: InternalWalletState,
withdrawalGroupId: string,
cancellationToken: CancellationToken,
-): Promise<{ ready: boolean }> {
+): Promise<TaskRunResult> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
@@ -1283,7 +1278,7 @@ async function queryReserve(
});
checkDbInvariant(!!withdrawalGroup);
if (withdrawalGroup.status !== WithdrawalGroupStatus.PendingQueryingStatus) {
- return { ready: true };
+ return TaskRunResult.backoff();
}
const reservePub = withdrawalGroup.reservePub;
@@ -1312,7 +1307,7 @@ async function queryReserve(
`got reserve status error, EC=${result.talerErrorResponse.code}`,
);
if (resp.status === HttpStatusCode.NotFound) {
- return { ready: false };
+ return TaskRunResult.backoff();
} else {
throwUnexpectedRequestError(resp, result.talerErrorResponse);
}
@@ -1341,13 +1336,7 @@ async function queryReserve(
notifyTransition(ws, transactionId, transitionResult);
- return { ready: true };
-}
-
-enum BankStatusResultCode {
- Done = "done",
- Waiting = "waiting",
- Aborted = "aborted",
+ return TaskRunResult.backoff();
}
/**
@@ -1452,6 +1441,7 @@ async function transitionKycSatisfied(
async function processWithdrawalGroupPendingKyc(
ws: InternalWalletState,
withdrawalGroup: WithdrawalGroupRecord,
+ cancellationToken: CancellationToken,
): Promise<TaskRunResult> {
const userType = "individual";
const kycInfo = withdrawalGroup.kycPending;
@@ -1467,45 +1457,35 @@ async function processWithdrawalGroupPendingKyc(
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
- const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
- runLongpollAsync(ws, retryTag, async (cancellationToken) => {
- logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
- const kycStatusRes = await ws.http.fetch(url.href, {
- method: "GET",
- cancellationToken,
- });
- logger.info(
- `kyc long-polling response status: HTTP ${kycStatusRes.status}`,
- );
- if (
- kycStatusRes.status === HttpStatusCode.Ok ||
- //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
- // remove after the exchange is fixed or clarified
- kycStatusRes.status === HttpStatusCode.NoContent
- ) {
- await transitionKycSatisfied(ws, withdrawalGroup);
- return { ready: true };
- } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
- const kycUrl = kycStatus.kyc_url;
- if (typeof kycUrl === "string") {
- await transitionKycUrlUpdate(ws, withdrawalGroupId, kycUrl);
- }
- return { ready: false };
- } else if (
- kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
- ) {
- const kycStatus = await kycStatusRes.json();
- logger.info(`aml status: ${j2s(kycStatus)}`);
- return { ready: false };
- } else {
- throw Error(
- `unexpected response from kyc-check (${kycStatusRes.status})`,
- );
- }
+ logger.info(`long-polling for withdrawal KYC status via ${url.href}`);
+ const kycStatusRes = await ws.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken,
});
- return TaskRunResult.longpoll();
+ logger.info(`kyc long-polling response status: HTTP ${kycStatusRes.status}`);
+ if (
+ kycStatusRes.status === HttpStatusCode.Ok ||
+ //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
+ // remove after the exchange is fixed or clarified
+ kycStatusRes.status === HttpStatusCode.NoContent
+ ) {
+ await transitionKycSatisfied(ws, withdrawalGroup);
+ } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`kyc status: ${j2s(kycStatus)}`);
+ const kycUrl = kycStatus.kyc_url;
+ if (typeof kycUrl === "string") {
+ await transitionKycUrlUpdate(ws, withdrawalGroupId, kycUrl);
+ }
+ } else if (
+ kycStatusRes.status === HttpStatusCode.UnavailableForLegalReasons
+ ) {
+ const kycStatus = await kycStatusRes.json();
+ logger.info(`aml status: ${j2s(kycStatus)}`);
+ } else {
+ throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
+ }
+ return TaskRunResult.backoff();
}
async function processWithdrawalGroupPendingReady(
@@ -1666,12 +1646,13 @@ async function processWithdrawalGroupPendingReady(
};
}
- return TaskRunResult.finished();
+ return TaskRunResult.backoff();
}
export async function processWithdrawalGroup(
ws: InternalWalletState,
withdrawalGroupId: string,
+ cancellationToken: CancellationToken,
): Promise<TaskRunResult> {
logger.trace("processing withdrawal group", withdrawalGroupId);
const withdrawalGroup = await ws.db
@@ -1684,54 +1665,30 @@ export async function processWithdrawalGroup(
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
- const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
-
- // We're already running!
- if (ws.activeLongpoll[retryTag]) {
- logger.info("withdrawal group already in long-polling, returning!");
- return {
- type: TaskRunResultType.Longpoll,
- };
- }
-
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
await processReserveBankStatus(ws, withdrawalGroupId);
// FIXME: This will get called by the main task loop, why call it here?!
- return await processWithdrawalGroup(ws, withdrawalGroupId);
- case WithdrawalGroupStatus.PendingQueryingStatus: {
- runLongpollAsync(ws, retryTag, (ct) => {
- return queryReserve(ws, withdrawalGroupId, ct);
- });
- logger.trace(
- "returning early from withdrawal for long-polling in background",
+ return await processWithdrawalGroup(
+ ws,
+ withdrawalGroupId,
+ cancellationToken,
);
- return {
- type: TaskRunResultType.Longpoll,
- };
+ case WithdrawalGroupStatus.PendingQueryingStatus: {
+ return queryReserve(ws, withdrawalGroupId, cancellationToken);
}
case WithdrawalGroupStatus.PendingWaitConfirmBank: {
- const res = await processReserveBankStatus(ws, withdrawalGroupId);
- switch (res.status) {
- case BankStatusResultCode.Aborted:
- case BankStatusResultCode.Done:
- return TaskRunResult.finished();
- case BankStatusResultCode.Waiting: {
- return TaskRunResult.pending();
- }
- }
- break;
- }
- case WithdrawalGroupStatus.Done:
- case WithdrawalGroupStatus.FailedBankAborted: {
- // FIXME
- return TaskRunResult.pending();
+ return await processReserveBankStatus(ws, withdrawalGroupId);
}
case WithdrawalGroupStatus.PendingAml:
// FIXME: Handle this case, withdrawal doesn't support AML yet.
- return TaskRunResult.pending();
+ return TaskRunResult.backoff();
case WithdrawalGroupStatus.PendingKyc:
- return processWithdrawalGroupPendingKyc(ws, withdrawalGroup);
+ return processWithdrawalGroupPendingKyc(
+ ws,
+ withdrawalGroup,
+ cancellationToken,
+ );
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
@@ -1747,6 +1704,8 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.SuspendedReady:
case WithdrawalGroupStatus.SuspendedRegisteringBank:
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.FailedBankAborted:
// Nothing to do.
return TaskRunResult.finished();
default:
@@ -2168,14 +2127,10 @@ async function registerReserveWithBank(
notifyTransition(ws, transactionId, transitionInfo);
}
-interface BankStatusResult {
- status: BankStatusResultCode;
-}
-
async function processReserveBankStatus(
ws: InternalWalletState,
withdrawalGroupId: string,
-): Promise<BankStatusResult> {
+): Promise<TaskRunResult> {
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
@@ -2188,9 +2143,7 @@ async function processReserveBankStatus(
case WithdrawalGroupStatus.PendingRegisteringBank:
break;
default:
- return {
- status: BankStatusResultCode.Done,
- };
+ return TaskRunResult.backoff();
}
if (
@@ -2200,9 +2153,7 @@ async function processReserveBankStatus(
}
const bankInfo = withdrawalGroup.wgInfo.bankInfo;
if (!bankInfo) {
- return {
- status: BankStatusResultCode.Done,
- };
+ return TaskRunResult.backoff();
}
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
@@ -2246,9 +2197,7 @@ async function processReserveBankStatus(
};
});
notifyTransition(ws, transactionId, transitionInfo);
- return {
- status: BankStatusResultCode.Aborted,
- };
+ return TaskRunResult.finished();
}
// Bank still needs to know our reserve info
@@ -2302,15 +2251,7 @@ async function processReserveBankStatus(
notifyTransition(ws, transactionId, transitionInfo);
- if (status.transfer_done) {
- return {
- status: BankStatusResultCode.Done,
- };
- } else {
- return {
- status: BankStatusResultCode.Waiting,
- };
- }
+ return TaskRunResult.backoff();
}
export interface PrepareCreateWithdrawalGroupResult {
@@ -2492,6 +2433,13 @@ export async function internalPerformCreateWithdrawalGroup(
prep.withdrawalGroup.exchangeBaseUrl,
);
+ const ctx = new WithdrawTransactionContext(
+ ws,
+ withdrawalGroup.withdrawalGroupId,
+ );
+
+ ws.taskScheduler.startShepherdTask(ctx.taskId);
+
return {
withdrawalGroup,
transitionInfo,
@@ -2619,10 +2567,10 @@ export async function acceptWithdrawalFromUri(
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
+
+ const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
+
+ const transactionId = ctx.transactionId;
// We do this here, as the reserve should be registered before we return,
// so that we can redirect the user to the bank's status page.
@@ -2639,7 +2587,7 @@ export async function acceptWithdrawalFromUri(
);
}
- ws.workAvailable.trigger();
+ ws.taskScheduler.startShepherdTask(ctx.taskId);
return {
reservePub: withdrawalGroup.reservePub,
@@ -2795,10 +2743,9 @@ export async function createManualWithdrawal(
});
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
- withdrawalGroupId,
- });
+ const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
+
+ const transactionId = ctx.transactionId;
const exchangePaytoUris = await ws.db
.mktx((x) => [x.withdrawalGroups, x.exchanges, x.exchangeDetails])
@@ -2806,7 +2753,7 @@ export async function createManualWithdrawal(
return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
});
- ws.workAvailable.trigger();
+ ws.taskScheduler.startShepherdTask(ctx.taskId);
return {
reservePub: withdrawalGroup.reservePub,