summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-02-19 20:50:03 +0100
committerFlorian Dold <florian@dold.me>2024-02-19 20:50:03 +0100
commit20397e3fba3fe4b274354047f76e3a8f3a92d6b8 (patch)
treee1e79fec4be55b2bb22e6830cea6c2afd5ea6ead
parent151ab351f6a2e00caecc9a1dda19b724f90a088e (diff)
downloadwallet-core-20397e3fba3fe4b274354047f76e3a8f3a92d6b8.tar.gz
wallet-core-20397e3fba3fe4b274354047f76e3a8f3a92d6b8.tar.bz2
wallet-core-20397e3fba3fe4b274354047f76e3a8f3a92d6b8.zip
wallet-core: long-poll withdrawal operation status
-rw-r--r--packages/taler-util/src/taler-types.ts43
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts190
2 files changed, 167 insertions, 66 deletions
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 1bdcf3fb1..4a0c53a79 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -27,9 +27,9 @@
import { Amounts, codecForAmountString } from "./amounts.js";
import {
+ Codec,
buildCodecForObject,
buildCodecForUnion,
- Codec,
codecForAny,
codecForBoolean,
codecForConstNumber,
@@ -45,14 +45,14 @@ import { strcmp } from "./helpers.js";
import {
CurrencySpecification,
codecForCurrencySpecificiation,
+ codecForEither,
} from "./index.js";
-import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js";
+import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
- codecForAbsoluteTime,
- codecForDuration,
- codecForTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
} from "./time.js";
/**
@@ -1228,9 +1228,42 @@ export interface MerchantOrderStatusUnpaid {
* POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
*/
export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected or pending.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+
+ // Deprecated field use status instead
+ // The transfer has been confirmed and registered by the bank.
+ // Does not guarantee that the funds have arrived at the exchange already.
transfer_done: boolean;
}
+export const codeForBankWithdrawalOperationPostResponse =
+ (): Codec<BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .property("transfer_done", codecForBoolean())
+ .build("BankWithdrawalOperationPostResponse");
+
export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey {
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 45792af41..9cf1ad36d 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -70,7 +70,7 @@ import {
WithdrawalExchangeAccountDetails,
addPaytoQueryParams,
canonicalizeBaseUrl,
- codecForAny,
+ codeForBankWithdrawalOperationPostResponse,
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeWithdrawBatchResponse,
@@ -1257,7 +1257,7 @@ async function updateWithdrawalDenoms(
* and are big enough to withdraw with available denominations,
* create a new withdrawal group for the remaining amount.
*/
-async function queryReserve(
+async function processQueryReserve(
ws: InternalWalletState,
withdrawalGroupId: string,
cancellationToken: CancellationToken,
@@ -1330,7 +1330,11 @@ async function queryReserve(
notifyTransition(ws, transactionId, transitionResult);
- return TaskRunResult.backoff();
+ if (transitionResult) {
+ return TaskRunResult.progress();
+ } else {
+ return TaskRunResult.backoff();
+ }
}
/**
@@ -1664,9 +1668,13 @@ export async function processWithdrawalGroup(
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
- return await processReserveBankStatus(ws, withdrawalGroupId);
+ return await processBankRegisterReserve(
+ ws,
+ withdrawalGroupId,
+ cancellationToken,
+ );
case WithdrawalGroupStatus.PendingQueryingStatus:
- return queryReserve(ws, withdrawalGroupId, cancellationToken);
+ return processQueryReserve(ws, withdrawalGroupId, cancellationToken);
case WithdrawalGroupStatus.PendingWaitConfirmBank:
return await processReserveBankStatus(ws, withdrawalGroupId);
case WithdrawalGroupStatus.PendingAml:
@@ -2064,7 +2072,7 @@ async function registerReserveWithBank(
if (
withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
) {
- throw Error("expecting withdrarwal type = bank integrated");
+ throw Error("expecting withdrawal type = bank integrated");
}
const bankInfo = withdrawalGroup.wgInfo.bankInfo;
if (!bankInfo) {
@@ -2081,8 +2089,10 @@ async function registerReserveWithBank(
body: reqBody,
timeout: getReserveRequestTimeout(withdrawalGroup),
});
- // FIXME: libeufin-bank currently doesn't return a response in the right format, so we don't validate at all.
- await readSuccessResponseJsonOrThrow(httpResp, codecForAny());
+ const status = await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codeForBankWithdrawalOperationPostResponse(),
+ );
const transitionInfo = await ws.db.runReadWriteTx(
["withdrawalGroups"],
async (tx) => {
@@ -2105,6 +2115,7 @@ async function registerReserveWithBank(
);
const oldTxState = computeWithdrawalTransactionStatus(r);
r.status = WithdrawalGroupStatus.PendingWaitConfirmBank;
+ r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
return {
@@ -2117,25 +2128,109 @@ async function registerReserveWithBank(
notifyTransition(ws, transactionId, transitionInfo);
}
-async function processReserveBankStatus(
+async function transitionBankAborted(
+ ctx: WithdrawTransactionContext,
+): Promise<TaskRunResult> {
+ logger.info("bank aborted the withdrawal");
+ const transitionInfo = await ctx.ws.db.runReadWriteTx(
+ ["withdrawalGroups"],
+ async (tx) => {
+ const r = await tx.withdrawalGroups.get(ctx.withdrawalGroupId);
+ if (!r) {
+ return;
+ }
+ switch (r.status) {
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ break;
+ default:
+ return;
+ }
+ if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
+ throw Error("invariant failed");
+ }
+ const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
+ const oldTxState = computeWithdrawalTransactionStatus(r);
+ r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
+ r.status = WithdrawalGroupStatus.FailedBankAborted;
+ const newTxState = computeWithdrawalTransactionStatus(r);
+ await tx.withdrawalGroups.put(r);
+ return {
+ oldTxState,
+ newTxState,
+ };
+ },
+ );
+ notifyTransition(ctx.ws, ctx.transactionId, transitionInfo);
+ return TaskRunResult.finished();
+}
+
+async function processBankRegisterReserve(
ws: InternalWalletState,
withdrawalGroupId: string,
+ cancellationToken: CancellationToken,
): Promise<TaskRunResult> {
+ const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
- const transactionId = constructTransactionIdentifier({
- tag: TransactionType.Withdrawal,
+ if (!withdrawalGroup) {
+ return TaskRunResult.finished();
+ }
+
+ if (
+ withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
+ ) {
+ throw Error("wrong withdrawal record type");
+ }
+ const bankInfo = withdrawalGroup.wgInfo.bankInfo;
+ if (!bankInfo) {
+ throw Error("no bank info in bank-integrated withdrawal");
+ }
+
+ const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
+ }
+ const url = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+
+ const statusResp = await ws.http.fetch(url.href, {
+ timeout: getReserveRequestTimeout(withdrawalGroup),
+ cancellationToken,
+ });
+
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (status.aborted) {
+ return transitionBankAborted(ctx);
+ }
+
+ // FIXME: Put confirm transfer URL in the DB!
+
+ await registerReserveWithBank(ws, withdrawalGroupId);
+ return TaskRunResult.progress();
+}
+
+async function processReserveBankStatus(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<TaskRunResult> {
+ const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
withdrawalGroupId,
});
- switch (withdrawalGroup?.status) {
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- case WithdrawalGroupStatus.PendingRegisteringBank:
- break;
- default:
- return TaskRunResult.backoff();
+
+ if (!withdrawalGroup) {
+ return TaskRunResult.finished();
}
+ const ctx = new WithdrawTransactionContext(ws, withdrawalGroupId);
+
if (
withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
) {
@@ -2143,58 +2238,35 @@ async function processReserveBankStatus(
}
const bankInfo = withdrawalGroup.wgInfo.bankInfo;
if (!bankInfo) {
- return TaskRunResult.backoff();
+ throw Error("no bank info in bank-integrated withdrawal");
}
- const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
+ const uriResult = parseWithdrawUri(bankInfo.talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
+ }
+ const url = new URL(
+ `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+ url.searchParams.set("timeout_ms", "30000");
- const statusResp = await ws.http.fetch(bankStatusUrl, {
+ const statusResp = await ws.http.fetch(url.href, {
timeout: getReserveRequestTimeout(withdrawalGroup),
});
+
const status = await readSuccessResponseJsonOrThrow(
statusResp,
codecForWithdrawOperationStatusResponse(),
);
if (status.aborted) {
- logger.info("bank aborted the withdrawal");
- const transitionInfo = await ws.db.runReadWriteTx(
- ["withdrawalGroups"],
- async (tx) => {
- const r = await tx.withdrawalGroups.get(withdrawalGroupId);
- if (!r) {
- return;
- }
- switch (r.status) {
- case WithdrawalGroupStatus.PendingRegisteringBank:
- case WithdrawalGroupStatus.PendingWaitConfirmBank:
- break;
- default:
- return;
- }
- if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
- throw Error("invariant failed");
- }
- const now = AbsoluteTime.toPreciseTimestamp(AbsoluteTime.now());
- const oldTxState = computeWithdrawalTransactionStatus(r);
- r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
- r.status = WithdrawalGroupStatus.FailedBankAborted;
- const newTxState = computeWithdrawalTransactionStatus(r);
- await tx.withdrawalGroups.put(r);
- return {
- oldTxState,
- newTxState,
- };
- },
- );
- notifyTransition(ws, transactionId, transitionInfo);
- return TaskRunResult.finished();
+ return transitionBankAborted(ctx);
}
- // Bank still needs to know our reserve info
- if (!status.selection_done) {
- await registerReserveWithBank(ws, withdrawalGroupId);
- return TaskRunResult.progress();
+ if (!status.transfer_done) {
+ // FIXME: This is a long-poll result
+ return TaskRunResult.backoff();
}
const transitionInfo = await ws.db.runReadWriteTx(
@@ -2206,7 +2278,6 @@ async function processReserveBankStatus(
}
// Re-check reserve status within transaction
switch (r.status) {
- case WithdrawalGroupStatus.PendingRegisteringBank:
case WithdrawalGroupStatus.PendingWaitConfirmBank:
break;
default:
@@ -2222,9 +2293,6 @@ async function processReserveBankStatus(
r.wgInfo.bankInfo.timestampBankConfirmed = timestampPreciseToDb(now);
r.status = WithdrawalGroupStatus.PendingQueryingStatus;
} else {
- logger.trace("withdrawal: transfer not yet confirmed by bank");
- r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
- r.senderWire = status.sender_wire;
}
const newTxState = computeWithdrawalTransactionStatus(r);
await tx.withdrawalGroups.put(r);
@@ -2235,7 +2303,7 @@ async function processReserveBankStatus(
},
);
- notifyTransition(ws, transactionId, transitionInfo);
+ notifyTransition(ws, ctx.transactionId, transitionInfo);
if (transitionInfo) {
return TaskRunResult.progress();