taler-typescript-core

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

commit a11ac4deb887b6d10041c90ce8e65ea61d723365
parent fab7c16b7e39b8086a19e1630ceaea3bf5c90b66
Author: Florian Dold <dold@taler.net>
Date:   Thu, 23 Jul 2026 01:40:51 +0200

wallet: use typed exchange client for recoup

Diffstat:
Mpackages/taler-util/src/http-client/exchange-client.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/recoup.ts | 63+++++++++++++++++++++++++++------------------------------------
2 files changed, 90 insertions(+), 36 deletions(-)

diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -82,6 +82,9 @@ import { PurseConflict, PurseConflictPartial, PurseCreateSuccessResponse, + RecoupConfirmation, + RecoupRefreshRequest, + RecoupRequest, WalletKycCheckResponse, WalletKycRequest, codecForAccountKycStatus, @@ -107,7 +110,9 @@ import { codecForPurseConflict, codecForPurseConflictPartial, codecForPurseCreateSuccessResponse, + codecForRecoupConfirmation, } from "../types-taler-exchange.js"; +import { ReserveStatus, codecForReserveStatus } from "../ReserveStatus.js"; import { CacheEvictor, addLongPollingParam, @@ -1540,6 +1545,64 @@ export class TalerExchangeHttpClient { * * https://docs.taler.net/core/api-exchange.html#withdrawal */ + /** + * https://docs.taler.net/core/api-exchange.html#get--reserves-$RESERVE_PUB + */ + async getReserveStatus( + reservePub: string, + longpoll: boolean = false, + ): Promise< + OperationOk<ReserveStatus> | OperationFail<HttpStatusCode.NotFound> + > { + const resp = await this.fetch(`reserves/${reservePub}`, {}, longpoll); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForReserveStatus()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup + */ + async recoupCoin( + coinPub: string, + body: RecoupRequest, + ): Promise<OperationOk<RecoupConfirmation>> { + const resp = await this.fetch(`coins/${coinPub}/recoup`, { + method: "POST", + body, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRecoupConfirmation()); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-exchange.html#post--coins-$COIN_PUB-recoup-refresh + */ + async recoupRefreshCoin( + coinPub: string, + body: RecoupRefreshRequest, + ): Promise<OperationOk<RecoupConfirmation>> { + const resp = await this.fetch(`coins/${coinPub}/recoup-refresh`, { + method: "POST", + body, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForRecoupConfirmation()); + default: + return opUnknownHttpFailure(resp); + } + } + async withdraw(args: { body: ExchangeWithdrawRequest; }): Promise< diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts @@ -33,22 +33,17 @@ import { Transaction, TransactionIdStr, TransactionType, - URL, WalletNotification, checkDbInvariant, - codecForRecoupConfirmation, - codecForReserveStatus, encodeCrock, getRandomBytes, j2s, } from "@gnu-taler/taler-util"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { PendingTaskType, TaskIdStr, TaskRunResult, TransactionContext, - cancelableFetch, constructTaskIdentifier, } from "./common.js"; import { @@ -64,7 +59,11 @@ import { import { CoinSourceType } from "./db-indexeddb.js"; import { createRefreshGroup } from "./refresh.js"; import { constructTransactionIdentifier } from "./transactions.js"; -import { WalletExecutionContext, getDenomInfo } from "./wallet.js"; +import { + WalletExecutionContext, + getDenomInfo, + walletExchangeClient, +} from "./wallet.js"; import { internalCreateWithdrawalGroup } from "./withdraw.js"; import { WalletDbTransaction } from "./dbtx.js"; @@ -145,20 +144,14 @@ async function recoupRefreshCoin( denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, }); - const reqUrl = new URL( - `/coins/${coin.coinPub}/recoup-refresh`, - coin.exchangeBaseUrl, - ); logger.trace(`making recoup request for ${coin.coinPub}`); - const resp = await cancelableFetch(wex, reqUrl, { - method: "POST", - body: recoupRequest, - }); - const recoupConfirmation = await readSuccessResponseJsonOrThrow( - resp, - codecForRecoupConfirmation(), + const exchangeClient = walletExchangeClient(coin.exchangeBaseUrl, wex); + const recoupResp = await exchangeClient.recoupRefreshCoin( + coin.coinPub, + recoupRequest, ); + const recoupConfirmation = recoupResp.body; if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) { throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`); @@ -244,16 +237,13 @@ export async function recoupWithdrawCoin( denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, }); - const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl); - logger.trace(`requesting recoup via ${reqUrl.href}`); - const resp = await cancelableFetch(wex, reqUrl, { - method: "POST", - body: recoupRequest, - }); - const recoupConfirmation = await readSuccessResponseJsonOrThrow( - resp, - codecForRecoupConfirmation(), + logger.trace(`requesting recoup for coin ${coin.coinPub}`); + const exchangeClient = walletExchangeClient(coin.exchangeBaseUrl, wex); + const recoupResp = await exchangeClient.recoupCoin( + coin.coinPub, + recoupRequest, ); + const recoupConfirmation = recoupResp.body; logger.trace(`got recoup confirmation ${j2s(recoupConfirmation)}`); @@ -347,18 +337,19 @@ export async function processRecoupGroup( } for (const reservePub of reserveSet) { - const reserveUrl = new URL( - `reserves/${reservePub}`, - recoupGroup.exchangeBaseUrl, - ); - logger.info(`querying reserve status for recoup via ${reserveUrl}`); + logger.info(`querying reserve status for recoup of ${reservePub}`); - const resp = await cancelableFetch(wex, reserveUrl); - - const result = await readSuccessResponseJsonOrThrow( - resp, - codecForReserveStatus(), + const exchangeClient = walletExchangeClient( + recoupGroup.exchangeBaseUrl, + wex, ); + const reserveResp = await exchangeClient.getReserveStatus(reservePub); + if (reserveResp.case !== "ok") { + throw Error( + `could not query reserve status for recoup (${reserveResp.case})`, + ); + } + const result = reserveResp.body; await internalCreateWithdrawalGroup(wex, { amount: Amounts.parseOrThrow(result.balance), exchangeBaseUrl: recoupGroup.exchangeBaseUrl,