commit 3f03548047ffb0680f1a7629ef738fe3bcbb593d
parent 46a937f7ca608840e4648fb45138fb388e9e6026
Author: Florian Dold <dold@taler.net>
Date: Thu, 13 Aug 2026 12:46:56 +0200
wallet-core: route service clients through wallet HTTP
Diffstat:
4 files changed, 178 insertions(+), 7 deletions(-)
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
@@ -155,7 +155,9 @@ export async function withdrawCoin(args: {
secretSeed: encodeCrock(getRandomBytes(32)),
value: Amounts.parseOrThrow(denom.value),
});
- const exchangeClient = new TalerExchangeHttpClient(exchangeBaseUrl);
+ const exchangeClient = new TalerExchangeHttpClient(exchangeBaseUrl, {
+ httpClient: http,
+ });
const sigResp = await cryptoApi.signWithdrawal({
amount: Amounts.stringify(denom.value),
@@ -421,7 +423,9 @@ export async function refreshCoin(req: {
logger.info(`requesting melt: ${j2s(meltReqBody)}`);
- const exchangeClient = new TalerExchangeHttpClient(oldCoin.exchangeBaseUrl);
+ const exchangeClient = new TalerExchangeHttpClient(oldCoin.exchangeBaseUrl, {
+ httpClient: http,
+ });
const meltResponse = succeedOrThrow(
await exchangeClient.postMelt({ body: meltReqBody }),
diff --git a/packages/taler-wallet-core/src/donau.ts b/packages/taler-wallet-core/src/donau.ts
@@ -114,7 +114,9 @@ async function submitDonationReceipts(
const groups = groupDonauReceipts(receipts);
for (const group of groups) {
- const donauClient = new DonauHttpClient(group.donauBaseUrl);
+ const donauClient = new DonauHttpClient(group.donauBaseUrl, {
+ httpClient: wex.http,
+ });
const conf = succeedOrThrow(await donauClient.getConfig());
logger.info(
@@ -196,7 +198,9 @@ async function fetchDonauStatements(
const statements: DonauStatementItem[] = [];
const groups = groupDonauReceipts(receipts);
for (const group of groups) {
- const donauClient = new DonauHttpClient(group.donauBaseUrl);
+ const donauClient = new DonauHttpClient(group.donauBaseUrl, {
+ httpClient: wex.http,
+ });
const conf = succeedOrThrow(await donauClient.getConfig());
const stmt = succeedOrThrow(
@@ -440,7 +444,9 @@ export async function generateDonauPlanchets(
logger.info(`creating budi for ${j2s(res)}`);
- const client = new DonauHttpClient(res.donauBaseUrl);
+ const client = new DonauHttpClient(res.donauBaseUrl, {
+ httpClient: wex.http,
+ });
const keysResp = succeedOrThrow(await client.getKeys());
@@ -561,7 +567,9 @@ export async function acceptDonauBlindSigs(
);
}
- const client = new DonauHttpClient(donauBaseUrl);
+ const client = new DonauHttpClient(donauBaseUrl, {
+ httpClient: wex.http,
+ });
// FIXME: Take this from the database instead of querying each time.
const keysResp = succeedOrThrow(await client.getKeys());
diff --git a/packages/taler-wallet-core/src/http-injection.test.ts b/packages/taler-wallet-core/src/http-injection.test.ts
@@ -0,0 +1,157 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+ AmountString,
+ DenominationInfo,
+ WithdrawTestBalanceRequest,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "@gnu-taler/taler-util/http";
+import assert from "node:assert";
+import { test } from "node:test";
+import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
+import { CoinInfo, refreshCoin, withdrawCoin } from "./dbless.js";
+import { withdrawTestBalance } from "./testing.js";
+import { WalletExecutionContext } from "./wallet.js";
+
+class RejectingHttp implements HttpRequestLibrary {
+ requests: Array<{ url: string; options?: HttpRequestOptions }> = [];
+
+ constructor(private failure: Error) {}
+
+ async fetch(
+ url: string,
+ options?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ this.requests.push({ url, options });
+ throw this.failure;
+ }
+}
+
+function assertInjectedFailure(cause: unknown, failure: Error): boolean {
+ assert.equal(cause, failure);
+ return true;
+}
+
+test("test-balance withdrawal uses the wallet HTTP client", async () => {
+ const failure = new Error("injected wallet HTTP client");
+ const http = new RejectingHttp(failure);
+ const ws = { refcntIgnoreTos: 0 };
+ const wex = { http, ws } as unknown as WalletExecutionContext;
+
+ await assert.rejects(
+ withdrawTestBalance(wex, {
+ amount: "KUDOS:25",
+ corebankApiBaseUrl: "https://bank.example/",
+ exchangeBaseUrl: "https://exchange.example/",
+ } as WithdrawTestBalanceRequest),
+ (cause) => assertInjectedFailure(cause, failure),
+ );
+
+ assert.equal(http.requests.length, 1);
+ assert.equal(http.requests[0].url, "https://bank.example/accounts");
+ assert.equal(http.requests[0].options?.method, "POST");
+ assert.equal(ws.refcntIgnoreTos, 0);
+});
+
+test("DB-less withdrawal uses its supplied HTTP client", async () => {
+ const failure = new Error("injected DB-less withdrawal HTTP client");
+ const http = new RejectingHttp(failure);
+ const cryptoApi = {
+ async createPlanchet() {
+ return {
+ coinEv: "coin-ev",
+ denomPubHash: "denom-hash",
+ reservePub: "reserve-pub",
+ };
+ },
+ async signWithdrawal() {
+ return { sig: "reserve-signature" };
+ },
+ } as unknown as TalerCryptoInterface;
+ const denom = {
+ value: "TESTKUDOS:1" as AmountString,
+ feeWithdraw: "TESTKUDOS:0" as AmountString,
+ denomPubHash: "denom-hash",
+ denomPub: { cipher: "RSA" },
+ } as DenominationInfo;
+
+ await assert.rejects(
+ withdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePub: "reserve-pub",
+ reservePriv: "reserve-priv",
+ },
+ denom,
+ exchangeBaseUrl: "https://exchange.example/",
+ }),
+ (cause) => assertInjectedFailure(cause, failure),
+ );
+
+ assert.equal(http.requests.length, 1);
+ assert.equal(http.requests[0].url, "https://exchange.example/withdraw");
+ assert.equal(http.requests[0].options?.method, "POST");
+});
+
+test("DB-less refresh uses its supplied HTTP client", async () => {
+ const failure = new Error("injected DB-less refresh HTTP client");
+ const http = new RejectingHttp(failure);
+ const cryptoApi = {
+ async deriveRefreshSessionV3() {
+ return {
+ confirmSig: "confirm-signature",
+ hash: "refresh-hash",
+ meltValueWithFee: {
+ currency: "TESTKUDOS",
+ value: 1,
+ fraction: 0,
+ },
+ planchets: [[{ coinEv: "coin-ev" }]],
+ signatures: [],
+ };
+ },
+ } as unknown as TalerCryptoInterface;
+ const oldCoin = {
+ coinPub: "coin-pub",
+ coinPriv: "coin-priv",
+ exchangeBaseUrl: "https://exchange.example/",
+ denomSig: "denom-signature",
+ denomPubHash: "old-denom-hash",
+ feeRefresh: "TESTKUDOS:0",
+ maxAge: 0,
+ } as unknown as CoinInfo;
+ const newDenom = {
+ value: "TESTKUDOS:1" as AmountString,
+ feeWithdraw: "TESTKUDOS:0" as AmountString,
+ denomPubHash: "new-denom-hash",
+ denomPub: { cipher: "RSA" },
+ } as DenominationInfo;
+
+ await assert.rejects(
+ refreshCoin({ http, cryptoApi, oldCoin, newDenoms: [newDenom] }),
+ (cause) => assertInjectedFailure(cause, failure),
+ );
+
+ assert.equal(http.requests.length, 1);
+ assert.equal(http.requests[0].url, "https://exchange.example/melt");
+ assert.equal(http.requests[0].options?.method, "POST");
+});
diff --git a/packages/taler-wallet-core/src/testing.ts b/packages/taler-wallet-core/src/testing.ts
@@ -134,7 +134,9 @@ async function withdrawTestBalanceImpl(
`Registering bank user, bank access base url ${corebankApiBaseUrl}`,
);
- const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl);
+ const corebankClient = new TalerCorebankApiClient(corebankApiBaseUrl, {
+ httpClient: wex.http,
+ });
const bankUser = await corebankClient.createRandomBankUser();
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);