commit 784c73923e768dda7e3f04de802fc1144f164923
parent 48aa88180c7f08c056dd6214ec09b26722a19aa2
Author: Florian Dold <dold@taler.net>
Date: Thu, 23 Jul 2026 02:29:28 +0200
wallet: use typed bank-integration client in withdraw
Diffstat:
3 files changed, 89 insertions(+), 58 deletions(-)
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
@@ -122,6 +122,7 @@ export class TalerBankIntegrationHttpClient {
): Promise<
| OperationOk<BankWithdrawalOperationStatus>
| OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Conflict>
> {
const url = new URL(`withdrawal-operation/${pathSegment(woid)}`, this.baseUrl);
addLongPollingParam(url, params);
@@ -141,6 +142,7 @@ export class TalerBankIntegrationHttpClient {
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForBankWithdrawalOperationStatus());
case HttpStatusCode.NotFound:
+ case HttpStatusCode.Conflict:
return opKnownHttpFailure(resp.status, resp);
default:
return opUnknownHttpFailure(resp);
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -43,6 +43,7 @@ import {
PartialWalletRunConfig,
PerformanceTable,
Result,
+ TalerBankIntegrationHttpClient,
TalerError,
TalerErrorCode,
TalerExchangeHttpClient,
@@ -141,6 +142,18 @@ export function walletExchangeClient(
});
}
+export function walletBankIntegrationClient(
+ baseUrl: string,
+ wex: WalletExecutionContext,
+ timeout?: Duration,
+): TalerBankIntegrationHttpClient {
+ return new TalerBankIntegrationHttpClient(baseUrl, {
+ httpClient: wex.http,
+ cancellationToken: wex.cancellationToken,
+ timeout,
+ });
+}
+
export function walletMerchantClient(
baseUrl: string,
wex: WalletExecutionContext,
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -80,6 +80,7 @@ import {
TransactionType,
TransactionWithdrawal,
TransferOption,
+ BankWithdrawalOperationPostRequest,
LegitimizationNeededResponse,
URL,
UnblindedDenominationSignature,
@@ -93,8 +94,6 @@ import {
checkDbInvariant,
checkLogicInvariant,
checkProtocolInvariant,
- codecForBankWithdrawalOperationPostResponse,
- codecForBankWithdrawalOperationStatus,
codecForCashinConversionResponse,
codecForConversionBankConfig,
codecForExchangeLegacyWithdrawBatchResponse,
@@ -122,7 +121,6 @@ import {
TransactionContext,
augmentTransferOptions,
cancelableFetch,
- cancelableLongPoll,
constructTaskIdentifier,
genericWaitForState,
genericWaitForStateVal,
@@ -197,6 +195,7 @@ import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js";
import {
WalletExecutionContext,
getDenomInfo,
+ walletBankIntegrationClient,
walletExchangeClient,
} from "./wallet.js";
@@ -1129,20 +1128,21 @@ async function processWithdrawalGroupDialogProposed(
const wopid = parsedUri.withdrawalOperationId;
- const url = new URL(
- `withdrawal-operation/${wopid}`,
+ const bankClient = walletBankIntegrationClient(
parsedUri.bankIntegrationApiBaseUrl,
+ ctx.wex,
);
- url.searchParams.set("old_state", "pending");
-
- const resp = await cancelableLongPoll(ctx.wex, url);
+ const resp = await bankClient.getWithdrawalOperationById(wopid, {
+ old_state: "pending",
+ timeoutMs: 30000,
+ });
- switch (resp.status) {
+ switch (resp.case) {
case HttpStatusCode.NotFound: {
- // FIXME: Further inspect the error body
- const err = await readTalerErrorResponse(resp);
- logger.warn(`withdrawal operation not found, aborting: ${j2s(err)}`);
+ logger.warn(
+ `withdrawal operation not found, aborting: ${j2s(resp.detail)}`,
+ );
await transitionSimple(
ctx,
WithdrawalGroupStatus.DialogProposed,
@@ -1150,15 +1150,11 @@ async function processWithdrawalGroupDialogProposed(
);
break;
}
- case HttpStatusCode.Ok: {
+ case "ok": {
// 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.
- const body = await readSuccessResponseJsonOrThrow(
- resp,
- codecForBankWithdrawalOperationStatus(),
- );
- if (body.status !== "pending") {
+ if (resp.body.status !== "pending") {
await transitionSimple(
ctx,
WithdrawalGroupStatus.DialogProposed,
@@ -2191,13 +2187,24 @@ async function processWithdrawalGroupAbortingBank(
if (wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated) {
throw Error("invalid state (aborting(bank) without bank info");
}
- const abortUrl = getBankAbortUrl(wgInfo.bankInfo.talerWithdrawUri);
- logger.info(`aborting withdrawal at ${abortUrl}`);
- const abortResp = await cancelableFetch(wex, abortUrl, {
- method: "POST",
- body: {},
- });
- logger.info(`abort response status: ${abortResp.status}`);
+ const uriResult = Result.orUndefined(
+ TalerUris.parseRestricted(
+ wgInfo.bankInfo.talerWithdrawUri,
+ TalerUriAction.Withdraw,
+ ),
+ );
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${wgInfo.bankInfo.talerWithdrawUri}`);
+ }
+ logger.info(`aborting withdrawal ${uriResult.withdrawalOperationId}`);
+ const bankClient = walletBankIntegrationClient(
+ uriResult.bankIntegrationApiBaseUrl,
+ wex,
+ );
+ const abortResp = await bankClient.abortWithdrawalOperationById(
+ uriResult.withdrawalOperationId,
+ );
+ logger.info(`abort response status: ${abortResp.response.status}`);
await ctx.wex.runWalletDbTx(async (tx) => {
const [wg, h] = await ctx.getRecordHandle(tx);
@@ -3066,12 +3073,20 @@ async function registerReserveWithBank(
"BUG: Tried to register reserve with bank, but bankInfo unavailable",
);
}
- const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
- const reqBody = {
+ const uriResult = Result.orUndefined(
+ TalerUris.parseRestricted(bankInfo.talerWithdrawUri, TalerUriAction.Withdraw),
+ );
+ if (!uriResult) {
+ throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
+ }
+ if (!bankInfo.exchangePaytoUri) {
+ throw Error("BUG: no exchange payto URI for bank-integrated withdrawal");
+ }
+ const reqBody: BankWithdrawalOperationPostRequest = {
reserve_pub: withdrawalGroup.reservePub,
selected_exchange: bankInfo.exchangePaytoUri,
- } as any;
- if (isFlexibleAmount) {
+ };
+ if (isFlexibleAmount && withdrawalGroup.instructedAmount != null) {
reqBody.amount = withdrawalGroup.instructedAmount;
}
logger.trace(`isFlexibleAmount: ${isFlexibleAmount}`);
@@ -3089,25 +3104,32 @@ async function registerReserveWithBank(
return TaskRunResult.progress();
}
- const httpResp = await cancelableFetch(wex, bankStatusUrl, {
- method: "POST",
- body: reqBody,
- timeout: getReserveRequestTimeout(withdrawalGroup),
- });
+ const bankClient = walletBankIntegrationClient(
+ uriResult.bankIntegrationApiBaseUrl,
+ wex,
+ getReserveRequestTimeout(withdrawalGroup),
+ );
+ const completeResp = await bankClient.completeWithdrawalOperationById(
+ uriResult.withdrawalOperationId,
+ reqBody,
+ );
- switch (httpResp.status) {
- case HttpStatusCode.NotFound: {
- // FIXME: Inspect particular status code
- const err = await readTalerErrorResponse(httpResp);
- logger.warn(`withdrawal operation not found, aborting: ${j2s(err)}`);
+ switch (completeResp.case) {
+ case "ok":
+ break;
+ case HttpStatusCode.NotFound:
+ logger.warn(
+ `withdrawal operation not found, aborting: ${j2s(completeResp.detail)}`,
+ );
await transitionSimple(
ctx,
WithdrawalGroupStatus.PendingRegisteringBank,
WithdrawalGroupStatus.FailedBankAborted,
);
return TaskRunResult.progress();
- }
- case HttpStatusCode.Conflict:
+ default:
+ // Any conflict (abort/reserve-selection/duplicate/amount/...) means the
+ // bank won't accept our reserve for this operation: give up.
await transitionSimple(
ctx,
WithdrawalGroupStatus.PendingRegisteringBank,
@@ -3116,10 +3138,7 @@ async function registerReserveWithBank(
return TaskRunResult.progress();
}
- const bankWopStatus = await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForBankWithdrawalOperationPostResponse(),
- );
+ const bankWopStatus = completeResp.body;
return await ctx.wex.runWalletDbTx(async (tx) => {
const [r, h] = await ctx.getRecordHandle(tx);
@@ -3217,17 +3236,19 @@ async function processBankRegisterReserve(
if (!uriResult) {
throw Error(`can't parse withdrawal URL ${bankInfo.talerWithdrawUri}`);
}
- const url = new URL(
- `withdrawal-operation/${uriResult.withdrawalOperationId}`,
+ const bankClient = walletBankIntegrationClient(
uriResult.bankIntegrationApiBaseUrl,
+ wex,
+ getReserveRequestTimeout(withdrawalGroup),
+ );
+ const statusResp = await bankClient.getWithdrawalOperationById(
+ uriResult.withdrawalOperationId,
);
-
- const statusResp = await cancelableFetch(wex, url, {
- timeout: getReserveRequestTimeout(withdrawalGroup),
- });
// FIXME: Consider looking at the exact taler error code
- switch (statusResp.status) {
+ switch (statusResp.case) {
+ case "ok":
+ break;
case HttpStatusCode.NotFound:
await transitionSimple(
ctx,
@@ -3242,14 +3263,9 @@ async function processBankRegisterReserve(
WithdrawalGroupStatus.AbortedOtherWallet,
);
return TaskRunResult.progress();
- default:
- break;
}
- const status = await readSuccessResponseJsonOrThrow(
- statusResp,
- codecForBankWithdrawalOperationStatus(),
- );
+ const status = statusResp.body;
// Legacy libeufin-bank behavior
if (status.status === "aborted") {