taler-typescript-core

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

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:
Mpackages/taler-util/src/http-client/exchange-client.ts | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/deposits.ts | 95++++++++++++++++++++++++++++---------------------------------------------------
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); } }