aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/withdraw.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/withdraw.ts')
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts272
1 files changed, 229 insertions, 43 deletions
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index a55ada796..0597c051f 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -56,6 +56,7 @@ import {
Logger,
NotificationType,
ObservabilityEventType,
+ PrepareBankIntegratedWithdrawalResponse,
TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
@@ -79,7 +80,9 @@ import {
assertUnreachable,
canonicalizeBaseUrl,
checkDbInvariant,
+ checkLogicInvariant,
codeForBankWithdrawalOperationPostResponse,
+ codecForBankWithdrawalOperationStatus,
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeWithdrawBatchResponse,
@@ -154,6 +157,7 @@ import {
constructTransactionIdentifier,
isUnsuccessfulTransaction,
notifyTransition,
+ parseTransactionIdentifier,
} from "./transactions.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@@ -164,7 +168,7 @@ import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
/**
* Logger for this file.
*/
-const logger = new Logger("operations/withdraw.ts");
+const logger = new Logger("withdraw.ts");
/**
* Update the materialized withdrawal transaction based
@@ -466,13 +470,18 @@ export class WithdrawTransactionContext implements TransactionContext {
break;
case WithdrawalGroupStatus.SuspendedAbortingBank:
case WithdrawalGroupStatus.AbortingBank:
+ case WithdrawalGroupStatus.AbortedUserRefused:
// No transition needed, but not an error
return TransitionResult.stay();
+ case WithdrawalGroupStatus.DialogProposed:
+ newStatus = WithdrawalGroupStatus.AbortedUserRefused;
+ break;
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
// Not allowed
throw Error("abort not allowed in current state");
default:
@@ -658,6 +667,21 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Bank,
};
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.Refused,
+ };
+ case WithdrawalGroupStatus.DialogProposed:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ };
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ return {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.CompletedByOtherWallet,
+ };
}
}
@@ -702,14 +726,78 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.FailedAbortingBank:
- return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedExchange:
- return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
+ case WithdrawalGroupStatus.AbortedUserRefused:
return [TransactionAction.Delete];
+ case WithdrawalGroupStatus.DialogProposed:
+ return [TransactionAction.Abort];
}
}
+async function processWithdrawalGroupDialogProposed(
+ ctx: WithdrawTransactionContext,
+ withdrawalGroup: WithdrawalGroupRecord,
+): Promise<TaskRunResult> {
+ if (
+ withdrawalGroup.wgInfo.withdrawalType !==
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ throw new Error(
+ "processWithdrawalGroupDialogProposed called in unexpected state",
+ );
+ }
+
+ const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri;
+
+ const parsedUri = parseWithdrawUri(talerWithdrawUri);
+
+ checkLogicInvariant(!!parsedUri);
+
+ const wopid = parsedUri.withdrawalOperationId;
+
+ const url = new URL(
+ `withdrawal-operation/${wopid}`,
+ parsedUri.bankIntegrationApiBaseUrl,
+ );
+
+ url.searchParams.set("old_state", "pending");
+ url.searchParams.set("long_poll_ms", "30000");
+
+ const resp = await ctx.wex.http.fetch(url.href, {
+ method: "GET",
+ cancellationToken: ctx.wex.cancellationToken,
+ });
+
+ // If the bank claims that the withdrawal operation is already
+ // pending, but we're still in DialogProposed, some other wallet
+ // must've completed the withdrawal, we're giving up.
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ const body = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForBankWithdrawalOperationStatus(),
+ );
+ if (body.status !== "pending") {
+ await ctx.transition({}, async (rec) => {
+ switch (rec?.status) {
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.status = WithdrawalGroupStatus.AbortedOtherWallet;
+ return TransitionResult.transition(rec);
+ }
+ }
+ return TransitionResult.stay();
+ });
+ }
+ break;
+ }
+ }
+
+ return TaskRunResult.longpollReturnedPending();
+}
+
/**
* Get information about a withdrawal from
* a taler://withdraw URI by asking the bank.
@@ -1907,6 +1995,8 @@ export async function processWithdrawalGroup(
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
return await processBankRegisterReserve(wex, withdrawalGroupId);
@@ -1924,6 +2014,8 @@ export async function processWithdrawalGroup(
return await processWithdrawalGroupPendingReady(wex, withdrawalGroup);
case WithdrawalGroupStatus.AbortingBank:
return await processWithdrawalGroupAbortingBank(wex, withdrawalGroup);
+ case WithdrawalGroupStatus.DialogProposed:
+ return await processWithdrawalGroupDialogProposed(ctx, withdrawalGroup);
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.FailedAbortingBank:
@@ -1936,6 +2028,8 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
case WithdrawalGroupStatus.Done:
case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ case WithdrawalGroupStatus.AbortedOtherWallet:
// Nothing to do.
return TaskRunResult.finished();
default:
@@ -2073,12 +2167,6 @@ export interface GetWithdrawalDetailsForUriOpts {
notifyChangeFromPendingTimeoutMs?: number;
}
-type WithdrawalOperationMemoryMap = {
- [uri: string]: boolean | undefined;
-};
-
-const ongoingChecks: WithdrawalOperationMemoryMap = {};
-
/**
* Get more information about a taler://withdraw URI.
*
@@ -2119,37 +2207,6 @@ export async function getWithdrawalDetailsForUri(
);
});
- // FIXME: this should be removed after the extended version of
- // withdrawal state machine. issue #8099
- if (
- info.status === "pending" &&
- opts.notifyChangeFromPendingTimeoutMs !== undefined &&
- !ongoingChecks[talerWithdrawUri]
- ) {
- ongoingChecks[talerWithdrawUri] = true;
- const bankApi = new TalerBankIntegrationHttpClient(
- info.apiBaseUrl,
- wex.http,
- );
-
- bankApi
- .getWithdrawalOperationById(info.operationId, {
- old_state: "pending",
- timeoutMs: opts.notifyChangeFromPendingTimeoutMs,
- })
- .then((resp) => {
- if (resp.type === "ok" && resp.body.status !== "pending") {
- wex.ws.notify({
- type: NotificationType.WithdrawalOperationTransition,
- uri: talerWithdrawUri,
- });
- }
- })
- .finally(() => {
- ongoingChecks[talerWithdrawUri] = false;
- });
- }
-
return {
operationId: info.operationId,
confirmTransferUrl: info.confirmTransferUrl,
@@ -2731,6 +2788,135 @@ export async function internalCreateWithdrawalGroup(
return res.withdrawalGroup;
}
+export async function prepareBankIntegratedWithdrawal(
+ wex: WalletExecutionContext,
+ req: {
+ talerWithdrawUri: string;
+ selectedExchange: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+ },
+): Promise<PrepareBankIntegratedWithdrawalResponse> {
+ const existingWithdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ req.talerWithdrawUri,
+ );
+ },
+ );
+
+ if (existingWithdrawalGroup) {
+ let url: string | undefined;
+ if (
+ existingWithdrawalGroup.wgInfo.withdrawalType ===
+ WithdrawalRecordType.BankIntegrated
+ ) {
+ url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
+ }
+ return {
+ transactionId: constructTransactionIdentifier({
+ tag: TransactionType.Withdrawal,
+ withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId,
+ }),
+ };
+ }
+
+ const selectedExchange = canonicalizeBaseUrl(req.selectedExchange);
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
+
+ const withdrawInfo = await getBankWithdrawalInfo(
+ wex.http,
+ req.talerWithdrawUri,
+ );
+ const exchangePaytoUri = await getExchangePaytoUri(
+ wex,
+ selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+
+ const withdrawalAccountList = await fetchWithdrawalAccountInfo(
+ wex,
+ {
+ exchange,
+ instructedAmount: withdrawInfo.amount,
+ },
+ wex.cancellationToken,
+ );
+
+ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, {
+ amount: withdrawInfo.amount,
+ exchangeBaseUrl: req.selectedExchange,
+ wgInfo: {
+ withdrawalType: WithdrawalRecordType.BankIntegrated,
+ exchangeCreditAccounts: withdrawalAccountList,
+ bankInfo: {
+ exchangePaytoUri,
+ talerWithdrawUri: req.talerWithdrawUri,
+ confirmUrl: withdrawInfo.confirmTransferUrl,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ },
+ },
+ restrictAge: req.restrictAge,
+ forcedDenomSel: req.forcedDenomSel,
+ reserveStatus: WithdrawalGroupStatus.DialogProposed,
+ });
+
+ const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
+
+ const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId);
+
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+
+ return {
+ transactionId: ctx.transactionId,
+ };
+}
+
+export async function confirmWithdrawal(
+ wex: WalletExecutionContext,
+ transactionId: string,
+): Promise<void> {
+ const parsedTx = parseTransactionIdentifier(transactionId);
+ if (parsedTx?.tag !== TransactionType.Withdrawal) {
+ throw Error("invalid withdrawal transaction ID");
+ }
+ const withdrawalGroup = await wex.db.runReadOnlyTx(
+ { storeNames: ["withdrawalGroups"] },
+ async (tx) => {
+ return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
+ parsedTx.withdrawalGroupId,
+ );
+ },
+ );
+
+ if (!withdrawalGroup) {
+ throw Error("withdrawal group not found");
+ }
+
+ const ctx = new WithdrawTransactionContext(
+ wex,
+ withdrawalGroup.withdrawalGroupId,
+ );
+ ctx.transition({}, async (rec) => {
+ if (!rec) {
+ return TransitionResult.stay();
+ }
+ switch (rec.status) {
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
+ return TransitionResult.transition(rec);
+ }
+ default:
+ throw Error("unable to confirm withdrawal in current state");
+ }
+ });
+
+ await wex.taskScheduler.resetTaskRetries(ctx.taskId);
+ wex.taskScheduler.startShepherdTask(ctx.taskId);
+}
+
/**
* Accept a bank-integrated withdrawal.
*
@@ -2738,6 +2924,8 @@ export async function internalCreateWithdrawalGroup(
*
* Thus after this call returns, the withdrawal operation can be confirmed
* with the bank.
+ *
+ * @deprecated in favor of prepare/accept
*/
export async function acceptWithdrawalFromUri(
wex: WalletExecutionContext,
@@ -2779,7 +2967,7 @@ export async function acceptWithdrawalFromUri(
};
}
- await fetchFreshExchange(wex, selectedExchange);
+ const exchange = await fetchFreshExchange(wex, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(
wex.http,
req.talerWithdrawUri,
@@ -2790,8 +2978,6 @@ export async function acceptWithdrawalFromUri(
withdrawInfo.wireTypes,
);
- const exchange = await fetchFreshExchange(wex, selectedExchange);
-
const withdrawalAccountList = await fetchWithdrawalAccountInfo(
wex,
{