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:
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;
};