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:
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();
});