taler-typescript-core

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

commit 54cd1fe607714e924cc621eefdc719c2a8f80137
parent e98a3a049fe4d502e30583e1c3effc8106e7b72f
Author: Florian Dold <dold@taler.net>
Date:   Wed, 12 Aug 2026 10:30:25 +0200

wallet-core: filter default exchanges to presets

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.test.ts | 8++++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 13+++++++++++++
Mpackages/taler-wallet-core/src/requests.test.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-wallet-core/src/requests.ts | 9+++++++--
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 3++-
5 files changed, 117 insertions(+), 5 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.test.ts b/packages/taler-util/src/types-taler-wallet.test.ts @@ -18,6 +18,7 @@ import assert from "node:assert"; import { test } from "node:test"; import { + codecForGetDefaultExchangesRequest, codecForTestingWaitTransactionRequest, matchTransactionState, } from "./types-taler-wallet.js"; @@ -32,6 +33,13 @@ const pendingWithdraw = { minor: TransactionMinorState.Withdraw, }; +test("default exchange request codec accepts presetOnly", () => { + const codec = codecForGetDefaultExchangesRequest(); + assert.strictEqual(codec.decode({ presetOnly: true }).presetOnly, true); + assert.strictEqual(codec.decode({}).presetOnly, undefined); + assert.throws(() => codec.decode({ presetOnly: "yes" })); +}); + test("state pattern without working flag ignores it", (t) => { assert.strictEqual( matchTransactionState( diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -4748,6 +4748,19 @@ export enum FlightRecordEvent { WithdrawalRedenominate = "withdrawal-redenominate", } +export interface GetDefaultExchangesRequest { + /** + * Only return exchanges whose entry is still a preset entry. + */ + presetOnly?: boolean; +} + +export const codecForGetDefaultExchangesRequest = + (): Codec<GetDefaultExchangesRequest> => + buildCodecForObject<GetDefaultExchangesRequest>() + .property("presetOnly", codecOptional(codecForBoolean())) + .build("GetDefaultExchangesRequest"); + export interface GetDefaultExchangesResponse { defaultExchanges: { /** diff --git a/packages/taler-wallet-core/src/requests.test.ts b/packages/taler-wallet-core/src/requests.test.ts @@ -17,9 +17,18 @@ import assert from "node:assert"; import { test } from "node:test"; -import { ConfigRecord, ConfigRecordKey } from "./db-common.js"; +import { + ConfigRecord, + ConfigRecordKey, + ExchangeEntryDbRecordStatus, + ExchangeEntryDbUpdateStatus, + WalletExchangeEntry, +} from "./db-common.js"; import { WalletDbTransaction } from "./dbtx.js"; -import { handleHintApplicationResumed } from "./requests.js"; +import { + handleGetDefaultExchanges, + handleHintApplicationResumed, +} from "./requests.js"; import { WalletExecutionContext } from "./wallet.js"; interface TestContext { @@ -93,3 +102,79 @@ test("application-resumed hint reports DB failures independently", async () => { { dbWriteHealthy: true, dbReadHealthy: false }, ); }); + +function makeExchangeEntry( + baseUrl: string, + entryStatus: ExchangeEntryDbRecordStatus, +): WalletExchangeEntry { + return { + baseUrl, + entryStatus, + updateStatus: ExchangeEntryDbUpdateStatus.Initial, + presetType: "prod", + presetCurrencyHint: "TESTKUDOS", + detailsPointer: undefined, + tosAcceptedEtag: undefined, + tosAcceptedTimestamp: undefined, + tosCurrentEtag: undefined, + lastKeysEtag: undefined, + lastUpdate: undefined, + } as WalletExchangeEntry; +} + +function makeExchangeTestContext( + exchanges: WalletExchangeEntry[], +): WalletExecutionContext { + const tx = { + async getExchanges(): Promise<WalletExchangeEntry[]> { + return exchanges; + }, + async getExchange( + baseUrl: string, + ): Promise<WalletExchangeEntry | undefined> { + return exchanges.find((x) => x.baseUrl === baseUrl); + }, + async getOperationRetry(): Promise<undefined> { + return undefined; + }, + async getCurrencyInfo(): Promise<undefined> { + return undefined; + }, + } as unknown as WalletDbTransaction; + return { + async runWalletDbTx<T>( + f: (tx: WalletDbTransaction) => Promise<T>, + ): Promise<T> { + return await f(tx); + }, + ws: { + devExperimentState: {}, + }, + } as WalletExecutionContext; +} + +test("default exchanges can be restricted to preset entries", async () => { + const presetUrl = "https://preset.example/"; + const usedUrl = "https://used.example/"; + const ephemeralUrl = "https://ephemeral.example/"; + const wex = makeExchangeTestContext([ + makeExchangeEntry(presetUrl, ExchangeEntryDbRecordStatus.Preset), + makeExchangeEntry(usedUrl, ExchangeEntryDbRecordStatus.Used), + makeExchangeEntry(ephemeralUrl, ExchangeEntryDbRecordStatus.Ephemeral), + ]); + + const all = await handleGetDefaultExchanges(wex, {}); + assert.deepStrictEqual( + all.defaultExchanges.map((x) => x.talerUri), + [ + "taler://withdraw-exchange/preset.example/", + "taler://withdraw-exchange/used.example/", + ], + ); + + const presets = await handleGetDefaultExchanges(wex, { presetOnly: true }); + assert.deepStrictEqual( + presets.defaultExchanges.map((x) => x.talerUri), + ["taler://withdraw-exchange/preset.example/"], + ); +}); diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts @@ -71,6 +71,7 @@ import { GetChoicesForPaymentResult, GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, + GetDefaultExchangesRequest, GetDefaultExchangesResponse, GetDepositWireTypesForCurrencyRequest, GetDepositWireTypesForCurrencyResponse, @@ -193,6 +194,7 @@ import { codecForGetBankingChoicesForPaytoRequest, codecForGetChoicesForPaymentRequest, codecForGetCurrencyInfoRequest, + codecForGetDefaultExchangesRequest, codecForGetDepositWireTypesForCurrencyRequest, codecForGetDepositWireTypesRequest, codecForGetDonauStatementsRequest, @@ -2241,11 +2243,14 @@ export async function handleGetFlightRecords( export async function handleGetDefaultExchanges( wex: WalletExecutionContext, - _req: EmptyObject, + req: GetDefaultExchangesRequest, ): Promise<GetDefaultExchangesResponse> { const defaultExchanges: GetDefaultExchangesResponse["defaultExchanges"] = []; const myExchanges = await listExchanges(wex, { filterByType: "prod", + filterByExchangeEntryStatus: req.presetOnly + ? ExchangeEntryStatus.Preset + : undefined, }); for (const exch of myExchanges.exchanges) { switch (exch.exchangeEntryStatus) { @@ -2414,7 +2419,7 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { handler: handleTestingCorruptWithdrawalCoinSel, }, [WalletApiOperation.GetDefaultExchanges]: { - codec: codecForEmptyObject(), + codec: codecForGetDefaultExchangesRequest(), handler: handleGetDefaultExchanges, }, [WalletApiOperation.TestingGetFlightRecords]: { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -98,6 +98,7 @@ import { GetChoicesForPaymentResult, GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, + GetDefaultExchangesRequest, GetDefaultExchangesResponse, GetDepositWireTypesForCurrencyRequest, GetDepositWireTypesForCurrencyResponse, @@ -1180,7 +1181,7 @@ export type GetExchangeDetailedInfoOp = { */ export type GetDefaultExchangesOp = { op: WalletApiOperation.GetDefaultExchanges; - request: EmptyObject; + request: GetDefaultExchangesRequest; response: GetDefaultExchangesResponse; };