taler-typescript-core

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

commit 48aa88180c7f08c056dd6214ec09b26722a19aa2
parent a7dcb1af4f5aa2a6ef3348f55a665580e55c6a42
Author: Florian Dold <dold@taler.net>
Date:   Thu, 23 Jul 2026 02:18:44 +0200

wallet: use typed exchange client for withdraw and reserve status

Diffstat:
Mpackages/taler-util/src/http-client/exchange-client.ts | 17++++++++++++++++-
Mpackages/taler-wallet-core/src/withdraw.ts | 96+++++++++++++++++++++++++++++++++++--------------------------------------------
2 files changed, 58 insertions(+), 55 deletions(-)

diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -1727,18 +1727,33 @@ export class TalerExchangeHttpClient { body: ExchangeWithdrawRequest; }): Promise< | OperationOk<ExchangeWithdrawResponse> + | OperationAlternative< + HttpStatusCode.UnavailableForLegalReasons, + LegitimizationNeededResponse + > | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.Gone> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.InternalServerError> > { const url = new URL(`withdraw`, this.baseUrl); const resp = await this.fetch(url, { method: "POST", body: args.body, }); - // FIXME: Some documented cases are missing. switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForExchangeWithdrawResponse()); + case HttpStatusCode.UnavailableForLegalReasons: + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForLegitimizationNeededResponse(), + ); case HttpStatusCode.Forbidden: + case HttpStatusCode.Gone: + case HttpStatusCode.NotFound: + case HttpStatusCode.InternalServerError: return opKnownHttpFailure(resp.status, resp); default: return opUnknownHttpFailure(resp); 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, + LegitimizationNeededResponse, URL, UnblindedDenominationSignature, WithdrawUriInfoResponse, @@ -97,9 +98,7 @@ import { codecForCashinConversionResponse, codecForConversionBankConfig, codecForExchangeLegacyWithdrawBatchResponse, - codecForExchangeWithdrawResponse, codecForLegitimizationNeededResponse, - codecForReserveStatus, encodeCrock, getErrorDetailFromException, getRandomBytes, @@ -109,7 +108,6 @@ import { } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, - HttpResponse, readSuccessResponseJsonOrErrorCode, readSuccessResponseJsonOrThrow, readTalerErrorResponse, @@ -196,7 +194,11 @@ import { parseTransactionIdentifier, } from "./transactions.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js"; -import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; +import { + WalletExecutionContext, + getDenomInfo, + walletExchangeClient, +} from "./wallet.js"; /** * Logger for this file. @@ -1442,14 +1444,11 @@ interface WithdrawalBatchResult { async function transitionKycRequired( wex: WalletExecutionContext, withdrawalGroup: WalletWithdrawalGroup, - resp: HttpResponse, + legiRequiredResp: LegitimizationNeededResponse, startIdx: number, requestCoinIdxs: number[], ): Promise<void> { logger.info("withdrawal requires KYC"); - const respJson = await resp.json(); - const legiRequiredResp = - codecForLegitimizationNeededResponse().decode(respJson); const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); @@ -1594,10 +1593,13 @@ async function processPlanchetExchangeLegacyBatchRequest( timeout: Duration.fromSpec({ seconds: 40 }), }); if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { + const legiRequiredResp = codecForLegitimizationNeededResponse().decode( + await resp.json(), + ); await transitionKycRequired( wex, withdrawalGroup, - resp, + legiRequiredResp, 0, requestCoinIdxs, ); @@ -1732,8 +1734,6 @@ async function processPlanchetExchangeBatchRequest( // FIXME: handle individual error codes better! - const reqUrl = new URL(`withdraw`, withdrawalGroup.exchangeBaseUrl).href; - const sigResp = await wex.cryptoApi.signWithdrawal({ amount: Amounts.stringify(accAmount), fee: Amounts.stringify(accFee), @@ -1750,21 +1750,22 @@ async function processPlanchetExchangeBatchRequest( reserve_sig: sigResp.sig, }; + const exchangeClient = walletExchangeClient( + withdrawalGroup.exchangeBaseUrl, + wex, + Duration.fromSpec({ seconds: 40 }), + ); + try { - const resp = await wex.http.fetch(reqUrl, { - method: "POST", - body: batchReq, - cancellationToken: wex.cancellationToken, - timeout: Duration.fromSpec({ seconds: 40 }), - }); - if (resp.status === HttpStatusCode.UnavailableForLegalReasons) { - } - switch (resp.status) { + const resp = await exchangeClient.withdraw({ body: batchReq }); + switch (resp.case) { + case "ok": + break; case HttpStatusCode.UnavailableForLegalReasons: { await transitionKycRequired( wex, withdrawalGroup, - resp, + resp.body, 0, requestCoinIdxs, ); @@ -1778,21 +1779,19 @@ async function processPlanchetExchangeBatchRequest( case HttpStatusCode.NotFound: { // The concrete handling of the error // happens in the caller. - const e = await readTalerErrorResponse(resp); - await storeCoinError(e, requestCoinIdxs[0]); + await storeCoinError(resp.detail!, requestCoinIdxs[0]); return { batchResp: { ev_sigs: [] }, coinIdxs: [], }; } + default: + // Forbidden or any unexpected status: treated as an error below. + throwUnexpectedRequestError(resp.response, resp.detail!); } - const r = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeWithdrawResponse(), - ); return { coinIdxs: requestCoinIdxs, - batchResp: r, + batchResp: resp.body, }; } catch (e) { // Network error or unexpected response. @@ -2078,35 +2077,24 @@ async function processQueryReserve( const reservePub = withdrawalGroup.reservePub; - const reserveUrl = new URL( - `reserves/${reservePub}`, + const exchangeClient = walletExchangeClient( withdrawalGroup.exchangeBaseUrl, + wex, + getReserveRequestTimeout(withdrawalGroup), ); - logger.trace(`querying reserve status via ${reserveUrl.href}`); - const resp = await cancelableLongPoll(wex, reserveUrl, { - timeout: getReserveRequestTimeout(withdrawalGroup), - }); + logger.trace(`querying reserve status for ${reservePub}`); + const resp = await exchangeClient.getReserveStatus(reservePub, true); - logger.trace(`reserve status code: HTTP ${resp.status}`); + logger.trace(`reserve status code: HTTP ${resp.response.status}`); - const result = await readSuccessResponseJsonOrErrorCode( - resp, - codecForReserveStatus(), - ); - - if (result.isError) { - logger.trace( - `got reserve status error, EC=${result.talerErrorResponse.code}`, - ); - if (resp.status === HttpStatusCode.NotFound) { - return TaskRunResult.longpollReturnedPending(); - } else { - throwUnexpectedRequestError(resp, result.talerErrorResponse); - } + if (resp.case === HttpStatusCode.NotFound) { + logger.trace(`got reserve status error (not found)`); + return TaskRunResult.longpollReturnedPending(); } - logger.trace(`got reserve status ${j2s(result.response)}`); + const reserveStatus = resp.body; + logger.trace(`got reserve status ${j2s(reserveStatus)}`); // We only allow changing the amount *down*, so that user error // in the wire transfer won't result in a giant withdrawal. @@ -2115,7 +2103,7 @@ async function processQueryReserve( // (skipped denoms are not counted). let amountChanged = Amounts.cmp( - result.response.balance, + reserveStatus.balance, withdrawalGroup.denomsSel.totalWithdrawCost, ) === -1; @@ -2146,7 +2134,7 @@ async function processQueryReserve( if (wg.status !== WithdrawalGroupStatus.PendingQueryingStatus) { return TaskRunResult.backoff(); } - const lastOrigin = result.response.last_origin; + const lastOrigin = reserveStatus.last_origin; // If the withdrawal had external confirmation, we don't store the // bank account details learned via the reserve here. const externalConfirmation = @@ -2164,7 +2152,7 @@ async function processQueryReserve( currency, ); const denomsSel = selectWithdrawalDenominations( - Amounts.parseOrThrow(result.response.balance), + Amounts.parseOrThrow(reserveStatus.balance), candidates, ); wg.denomsSel = denomsSel; @@ -2172,7 +2160,7 @@ async function processQueryReserve( wg.effectiveWithdrawalAmount = denomsSel.totalCoinValue; } wg.status = WithdrawalGroupStatus.PendingReady; - wg.reserveBalanceAmount = Amounts.stringify(result.response.balance); + wg.reserveBalanceAmount = Amounts.stringify(reserveStatus.balance); await h.update(wg); return TaskRunResult.progress(); });