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:
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,