taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/dbless.ts | 8++++++--
Mpackages/taler-wallet-core/src/donau.ts | 16++++++++++++----
Apackages/taler-wallet-core/src/http-injection.test.ts | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/testing.ts | 4+++-
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)}`);