commit a7dcb1af4f5aa2a6ef3348f55a665580e55c6a42
parent 93e8f3df9703dd003e2ba2573f3e64cf6ddd5f9e
Author: Florian Dold <dold@taler.net>
Date: Thu, 23 Jul 2026 01:53:50 +0200
wallet: use typed exchange client for deposits
Diffstat:
2 files changed, 108 insertions(+), 62 deletions(-)
diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts
@@ -57,7 +57,9 @@ import {
AmlDecisionRequest,
AmlDecisionsResponse,
AvailableMeasureSummary,
+ BatchDepositSuccess,
EventCounter,
+ ExchangeBatchDepositRequest,
ExchangeGetContractResponse,
ExchangeKeysResponse,
ExchangeKycUploadFormRequest,
@@ -89,9 +91,12 @@ import {
RecoupConfirmation,
RecoupRefreshRequest,
RecoupRequest,
+ TrackTransactionAccepted,
+ TrackTransactionWired,
WalletKycCheckResponse,
WalletKycRequest,
codecForAccountKycStatus,
+ codecForBatchDepositSuccess,
codecForAmlDecisionsAccounts,
codecForAmlDecisionsResponse,
codecForAmlKycAttributes,
@@ -116,6 +121,8 @@ import {
codecForPurseConflictPartial,
codecForPurseCreateSuccessResponse,
codecForRecoupConfirmation,
+ codecForTackTransactionAccepted,
+ codecForTackTransactionWired,
} from "../types-taler-exchange.js";
import { ReserveStatus, codecForReserveStatus } from "../ReserveStatus.js";
import {
@@ -1648,6 +1655,74 @@ export class TalerExchangeHttpClient {
}
}
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--batch-deposit
+ */
+ async batchDeposit(args: {
+ body: ExchangeBatchDepositRequest;
+ }): Promise<
+ | OperationOk<BatchDepositSuccess>
+ | OperationAlternative<
+ HttpStatusCode.UnavailableForLegalReasons,
+ LegitimizationNeededResponse
+ >
+ > {
+ const resp = await this.fetch(`batch-deposit`, {
+ method: "POST",
+ body: args.body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForBatchDepositSuccess());
+ case HttpStatusCode.UnavailableForLegalReasons:
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForLegitimizationNeededResponse(),
+ );
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--deposits-$H_WIRE-$MERCHANT_PUB-$H_CONTRACT_TERMS-$COIN_PUB
+ *
+ * Long-polls until the deposit is wired (200) or the exchange reports a
+ * pending (202) state.
+ */
+ async trackDeposit(args: {
+ wireHash: string;
+ merchantPub: string;
+ contractTermsHash: string;
+ coinPub: string;
+ merchantSig: string;
+ }): Promise<
+ | OperationOk<TrackTransactionWired>
+ | OperationAlternative<HttpStatusCode.Accepted, TrackTransactionAccepted>
+ > {
+ const url = new URL(
+ `deposits/${args.wireHash}/${args.merchantPub}/${args.contractTermsHash}/${args.coinPub}`,
+ this.baseUrl,
+ );
+ url.searchParams.set("merchant_sig", args.merchantSig);
+ url.searchParams.set("lpt", "1");
+ const resp = await this.fetch(url, {}, true);
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTackTransactionWired());
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeHttpFailure(
+ resp,
+ resp.status,
+ codecForTackTransactionAccepted(),
+ );
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
async withdraw(args: {
body: ExchangeWithdrawRequest;
}): Promise<
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -70,23 +70,13 @@ import {
canonicalJson,
checkDbInvariant,
checkLogicInvariant,
- codecForBatchDepositSuccess,
- codecForLegitimizationNeededResponse,
codecForMerchantContractTerms,
- codecForTackTransactionAccepted,
- codecForTackTransactionWired,
encodeCrock,
getRandomBytes,
hashWire,
j2s,
} from "@gnu-taler/taler-util";
import {
- readResponseJsonOrThrow,
- readSuccessResponseJsonOrThrow,
- readTalerErrorResponse,
- throwUnexpectedRequestError,
-} from "@gnu-taler/taler-util/http";
-import {
getExchangesForDeposit,
selectPayCoins,
selectPayCoinsInTx,
@@ -98,8 +88,6 @@ import {
TaskRunResult,
TransactionContext,
augmentTransferOptions,
- cancelableFetch,
- cancelableLongPoll,
constructTaskIdentifier,
genericWaitForState,
getGenericRecordHandle,
@@ -160,7 +148,11 @@ import {
isUnsuccessfulTransaction,
parseTransactionIdentifier,
} from "./transactions.js";
-import { WalletExecutionContext, getDenomInfo } from "./wallet.js";
+import {
+ WalletExecutionContext,
+ getDenomInfo,
+ walletExchangeClient,
+} from "./wallet.js";
import { augmentPaytoUrisForKycTransfer } from "./withdraw.js";
/**
@@ -870,19 +862,16 @@ async function refundDepositGroup(
refund_amount: refundAmount,
rtransaction_id: rtid,
};
- const refundUrl = new URL(`coins/${coinPub}/refund`, coinExchange);
- const httpResp = await cancelableFetch(wex, refundUrl, {
- method: "POST",
- body: refundReq,
- });
+ const exchangeClient = walletExchangeClient(coinExchange, wex);
+ const refundResp = await exchangeClient.refundCoin(coinPub, refundReq);
logger.info(
- `coin ${i} refund HTTP status for coin: ${httpResp.status}`,
+ `coin ${i} refund HTTP status for coin: ${refundResp.response.status}`,
);
let newStatus: DepositElementStatus;
- if (httpResp.status === 200) {
+ if (refundResp.case === "ok") {
// FIXME: validate response
newStatus = DepositElementStatus.RefundSuccess;
- } else if (httpResp.status == 404) {
+ } else if (refundResp.case === HttpStatusCode.NotFound) {
// Exchange doesn't know about the deposit.
// It's possible that we already sent out the
// deposit request, but it didn't arrive yet,
@@ -1738,25 +1727,18 @@ async function submitDepositBatch(
// Check for cancellation before making network request.
wex.cancellationToken?.throwIfCancelled();
- const url = new URL(`batch-deposit`, exchangeBaseUrl);
- logger.info(`depositing to ${url.href}`);
+ logger.info(`depositing to ${exchangeBaseUrl}`);
logger.trace(`deposit request: ${j2s(batchReq)}`);
- const httpResp = await cancelableFetch(wex, url, {
- method: "POST",
- body: batchReq,
- });
+ const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex);
+ const depositResp = await exchangeClient.batchDeposit({ body: batchReq });
- logger.info(`deposit result status ${httpResp.status}`);
+ logger.info(`deposit result status ${depositResp.response.status}`);
- switch (httpResp.status) {
- case HttpStatusCode.Accepted:
- case HttpStatusCode.Ok:
+ switch (depositResp.case) {
+ case "ok":
break;
case HttpStatusCode.UnavailableForLegalReasons: {
- const kycLegiNeededResp = await readResponseJsonOrThrow(
- httpResp,
- codecForLegitimizationNeededResponse(),
- );
+ const kycLegiNeededResp = depositResp.body;
logger.info(
`kyc legitimization needed response: ${j2s(kycLegiNeededResp)}`,
);
@@ -1768,8 +1750,6 @@ async function submitDepositBatch(
}
}
- await readSuccessResponseJsonOrThrow(httpResp, codecForBatchDepositSuccess());
-
await wex.runWalletDbTx(async (tx) => {
const dg = await tx.getDepositGroup(depositGroupId);
if (!dg) {
@@ -1953,10 +1933,6 @@ async function trackDeposit(
depositGroup.wire.salt,
);
- const url = new URL(
- `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`,
- exchangeUrl,
- );
const sigResp = await wex.cryptoApi.signTrackTransaction({
coinPub,
contractTermsHash: depositGroup.contractTermsHash,
@@ -1964,33 +1940,28 @@ async function trackDeposit(
merchantPub: depositGroup.merchantPub,
wireHash,
});
- url.searchParams.set("merchant_sig", sigResp.sig);
- // wait for the a 202 state where kyc_ok is false or a 200 OK response
- url.searchParams.set("lpt", `1`);
- const httpResp = await cancelableLongPoll(wex, url);
- logger.trace(`deposits response status: ${httpResp.status}`);
- switch (httpResp.status) {
+ const exchangeClient = walletExchangeClient(exchangeUrl, wex);
+ // wait for a 202 state where kyc_ok is false or a 200 OK response
+ const trackResp = await exchangeClient.trackDeposit({
+ wireHash,
+ merchantPub: depositGroup.merchantPub,
+ contractTermsHash: depositGroup.contractTermsHash,
+ coinPub,
+ merchantSig: sigResp.sig,
+ });
+ logger.trace(`deposits response status: ${trackResp.response.status}`);
+ switch (trackResp.case) {
case HttpStatusCode.Accepted: {
- const accepted = await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForTackTransactionAccepted(),
- );
+ const accepted = trackResp.body;
logger.trace(`deposits response: ${j2s(accepted)}`);
return { type: "accepted", ...accepted };
}
- case HttpStatusCode.Ok: {
- const wired = await readSuccessResponseJsonOrThrow(
- httpResp,
- codecForTackTransactionWired(),
- );
+ case "ok": {
+ const wired = trackResp.body;
return { type: "wired", ...wired };
}
- default: {
- throwUnexpectedRequestError(
- httpResp,
- await readTalerErrorResponse(httpResp),
- );
- }
+ default:
+ assertUnreachable(trackResp);
}
}