taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/http-client/bank-integration.ts | 2++
Mpackages/taler-wallet-core/src/wallet.ts | 13+++++++++++++
Mpackages/taler-wallet-core/src/withdraw.ts | 132++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
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") {