taler-typescript-core

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

commit 50f619044ee616b3debd3b71cbe75db8a251ef79
parent ec031081eabb968456620332c1c6bfb84757978d
Author: Florian Dold <dold@taler.net>
Date:   Wed, 12 Aug 2026 13:29:55 +0200

wallet-core: control builtin exchange candidates

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.test.ts | 33+++++++++++++++++++++++++++++----
Mpackages/taler-util/src/types-taler-wallet.ts | 20+++++++++++++++++++-
Apackages/taler-wallet-core/src/builtin-exchanges.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/dev-experiments.ts | 6------
Mpackages/taler-wallet-core/src/exchanges.ts | 135++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mpackages/taler-wallet-core/src/preset-exchanges.ts | 46++--------------------------------------------
Mpackages/taler-wallet-core/src/requests.test.ts | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/taler-wallet-core/src/requests.ts | 49++++++-------------------------------------------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 6++++++
9 files changed, 376 insertions(+), 150 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.test.ts b/packages/taler-util/src/types-taler-wallet.test.ts @@ -24,14 +24,39 @@ import { matchTransactionState, } from "./types-taler-wallet.js"; -test("default and candidate exchange requests share presetOnly", () => { +test("default and candidate exchange requests share selection flags", () => { for (const codec of [ codecForGetDefaultExchangesRequest(), codecForListWithdrawalExchangeCandidatesRequest(), ]) { - assert.strictEqual(codec.decode({ presetOnly: true }).presetOnly, true); - assert.strictEqual(codec.decode({}).presetOnly, undefined); - assert.throws(() => codec.decode({ presetOnly: "yes" })); + assert.deepStrictEqual( + codec.decode({ + presetOnly: true, + withBuiltin: false, + withDemo: true, + withTest: true, + }), + { + presetOnly: true, + withBuiltin: false, + withDemo: true, + withTest: true, + }, + ); + assert.deepStrictEqual(codec.decode({}), { + presetOnly: undefined, + withBuiltin: undefined, + withDemo: undefined, + withTest: undefined, + }); + for (const property of [ + "presetOnly", + "withBuiltin", + "withDemo", + "withTest", + ]) { + assert.throws(() => codec.decode({ [property]: "yes" })); + } } }); import { diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -4764,14 +4764,29 @@ export enum FlightRecordEvent { * @deprecated Use {@link ListWithdrawalExchangeCandidatesRequest} instead. */ export interface GetDefaultExchangesRequest { - /** Only return exchanges whose entry is still a preset entry. */ + /** + * Only return production exchanges from the builtin exchange list. + * Cannot be combined with `withBuiltin: false`. + */ presetOnly?: boolean; + + /** Include exchanges from the builtin list. Defaults to true. */ + withBuiltin?: boolean; + + /** Include demo exchanges from the builtin list. Defaults to false. */ + withDemo?: boolean; + + /** Include test exchanges from the builtin list. Defaults to false. */ + withTest?: boolean; } export const codecForGetDefaultExchangesRequest = (): Codec<GetDefaultExchangesRequest> => buildCodecForObject<GetDefaultExchangesRequest>() .property("presetOnly", codecOptional(codecForBoolean())) + .property("withBuiltin", codecOptional(codecForBoolean())) + .property("withDemo", codecOptional(codecForBoolean())) + .property("withTest", codecOptional(codecForBoolean())) .build("GetDefaultExchangesRequest"); /** @@ -4803,6 +4818,9 @@ export const codecForListWithdrawalExchangeCandidatesRequest = (): Codec<ListWithdrawalExchangeCandidatesRequest> => buildCodecForObject<ListWithdrawalExchangeCandidatesRequest>() .property("presetOnly", codecOptional(codecForBoolean())) + .property("withBuiltin", codecOptional(codecForBoolean())) + .property("withDemo", codecOptional(codecForBoolean())) + .property("withTest", codecOptional(codecForBoolean())) .build("ListWithdrawalExchangeCandidatesRequest"); export enum ExchangeRecommendationReason { diff --git a/packages/taler-wallet-core/src/builtin-exchanges.ts b/packages/taler-wallet-core/src/builtin-exchanges.ts @@ -0,0 +1,60 @@ +/* + 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 { CurrencySpecification } from "@gnu-taler/taler-util"; + +export interface BuiltinExchange { + exchangeBaseUrl: string; + currencyHint: string; + currencySpec?: CurrencySpecification; + type: "demo" | "prod" | "test"; + versionAdded: number; +} + +/** + * Exchanges that ship with wallet-core. + */ +export const builtinExchanges: BuiltinExchange[] = [ + { + exchangeBaseUrl: "https://exchange.demo.taler.net/", + type: "demo", + currencyHint: "KUDOS", + versionAdded: 1, + }, + { + exchangeBaseUrl: "https://exchange.test.taler.net/", + type: "test", + currencyHint: "KUDOS", + versionAdded: 1, + }, + { + exchangeBaseUrl: "https://exchange.taler-ops.ch/", + currencyHint: "CHF", + type: "prod", + versionAdded: 3, + currencySpec: { + name: "Swiss francs", + common_amounts: ["CHF:5", "CHF:10", "CHF:25", "CHF:50"], + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": "Fr.", + "-2": "Rp.", + }, + }, + }, +]; diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -122,8 +122,6 @@ export interface DevExperimentState { fakeDemoShortcuts?: AmountString[]; - fakeDefaultExchangeDemo?: boolean; - blockPayResponse?: boolean; blockClaimResponse?: boolean; @@ -506,10 +504,6 @@ export async function applyDevExperiment( } return; } - case "default-exchange-demo": { - wex.ws.devExperimentState.fakeDefaultExchangeDemo = getValFlag(parsedUri); - return; - } case "block-pay-response": { const val = getValFlag(parsedUri); logger.info(`setting dev experiment blockPayResponse=${val}`); diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -101,6 +101,7 @@ import { TransactionType, URL, WalletKycRequest, + WithdrawalExchangeCandidate, WireFee, WireFeeMap, WireInfo, @@ -122,6 +123,7 @@ import { HttpRequestLibrary, throwUnexpectedRequestError, } from "@gnu-taler/taler-util/http"; +import { builtinExchanges, type BuiltinExchange } from "./builtin-exchanges.js"; import { PendingTaskType, TaskIdStr, @@ -3488,6 +3490,44 @@ function hasPresetMetadata(record: WalletExchangeEntry): boolean { ); } +function builtinTypeRequested( + type: BuiltinExchange["type"], + req: ListWithdrawalExchangeCandidatesRequest, +): boolean { + switch (type) { + case "prod": + return true; + case "demo": + return req.withDemo === true; + case "test": + return req.withTest === true; + } +} + +function makeBuiltinExchangeCandidate( + exchange: BuiltinExchange, +): WithdrawalExchangeCandidate { + return { + talerUri: TalerUris.stringify({ + type: TalerUriAction.WithdrawExchange, + exchangeBaseUrl: exchange.exchangeBaseUrl as HostPortPath, + }), + exchangeBaseUrl: exchange.exchangeBaseUrl, + currency: exchange.currencyHint, + currencySpec: exchange.currencySpec ?? { + alt_unit_names: {}, + name: exchange.currencyHint, + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + }, + exchangeEntryStatus: ExchangeEntryStatus.Preset, + exchangeUpdateStatus: ExchangeUpdateStatus.Initial, + source: ExchangeEntrySource.Builtin, + recommendationReasons: [ExchangeRecommendationReason.Preset], + }; +} + /** * List exchanges suitable for presentation in a withdrawal chooser. */ @@ -3495,18 +3535,41 @@ export async function listWithdrawalExchangeCandidates( wex: WalletExecutionContext, req: ListWithdrawalExchangeCandidatesRequest, ): Promise<ListWithdrawalExchangeCandidatesResponse> { - const items = await listExchangeItemsInternal(wex, { - filterByType: "prod", - filterByExchangeEntryStatus: req.presetOnly - ? ExchangeEntryStatus.Preset - : undefined, - }); - const candidates = items - .filter( - ({ item }) => - item.exchangeEntryStatus !== ExchangeEntryStatus.Ephemeral && - item.currency !== "UNKNOWN", - ) + if (req.presetOnly && req.withBuiltin === false) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, + { parameter: "withBuiltin" }, + "presetOnly cannot be combined with withBuiltin=false", + ); + } + + const builtinByUrl = new Map( + builtinExchanges.map((exchange) => [exchange.exchangeBaseUrl, exchange]), + ); + const items = await listExchangeItemsInternal(wex, {}); + const candidatesByUrl = new Map<string, WithdrawalExchangeCandidate>(); + const databaseCandidates = items + .filter(({ item, record }) => { + if ( + item.exchangeEntryStatus === ExchangeEntryStatus.Ephemeral || + item.currency === "UNKNOWN" + ) { + return false; + } + const builtin = builtinByUrl.get(item.exchangeBaseUrl); + if (req.presetOnly) { + return builtin?.type === "prod"; + } + const presetType = builtin?.type ?? record.presetType; + if ( + presetType === "demo" || + presetType === "test" || + presetType === "prod" + ) { + return builtinTypeRequested(presetType, req); + } + return true; + }) .map(({ item, record }) => { const source = getExchangeEntrySource(record); const recommendationReasons: ExchangeRecommendationReason[] = []; @@ -3541,9 +3604,33 @@ export async function listWithdrawalExchangeCandidates( ...(item.lastWithdrawal != null ? { lastWithdrawal: item.lastWithdrawal } : undefined), - }; + } satisfies WithdrawalExchangeCandidate; }); + for (const candidate of databaseCandidates) { + candidatesByUrl.set(candidate.exchangeBaseUrl, candidate); + } + + if (req.withBuiltin !== false) { + for (const exchange of builtinExchanges) { + if ( + req.presetOnly + ? exchange.type !== "prod" + : !builtinTypeRequested(exchange.type, req) + ) { + continue; + } + if (!candidatesByUrl.has(exchange.exchangeBaseUrl)) { + candidatesByUrl.set( + exchange.exchangeBaseUrl, + makeBuiltinExchangeCandidate(exchange), + ); + } + } + } + + const candidates = [...candidatesByUrl.values()]; + candidates.sort((a, b) => { if (a.lastWithdrawal != null || b.lastWithdrawal != null) { if (a.lastWithdrawal == null) return 1; @@ -3571,28 +3658,6 @@ export async function listWithdrawalExchangeCandidates( ); }); - if (wex.ws.devExperimentState.fakeDefaultExchangeDemo) { - candidates.push({ - talerUri: TalerUris.stringify({ - type: TalerUriAction.WithdrawExchange, - exchangeBaseUrl: "https://exchange.demo.taler.net/" as HostPortPath, - }), - exchangeBaseUrl: "https://exchange.demo.taler.net/", - currency: "KUDOS", - currencySpec: { - name: "Kudos", - common_amounts: ["KUDOS:5", "KUDOS:10", "KUDOS:25", "KUDOS:50"], - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { "0": "ク" }, - }, - exchangeEntryStatus: ExchangeEntryStatus.Preset, - exchangeUpdateStatus: ExchangeUpdateStatus.Ready, - source: ExchangeEntrySource.Builtin, - recommendationReasons: [ExchangeRecommendationReason.Preset], - }); - } return { candidates }; } diff --git a/packages/taler-wallet-core/src/preset-exchanges.ts b/packages/taler-wallet-core/src/preset-exchanges.ts @@ -14,19 +14,12 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { CurrencySpecification, Logger } from "@gnu-taler/taler-util"; +import { Logger } from "@gnu-taler/taler-util"; +import { builtinExchanges } from "./builtin-exchanges.js"; import { ConfigRecordKey } from "./db-common.js"; import { putPresetExchangeEntry } from "./exchanges.js"; import { WalletExecutionContext } from "./wallet.js"; -interface BuiltinExchange { - exchangeBaseUrl: string; - currencyHint: string; - currencySpec?: CurrencySpecification; - type: "demo" | "prod" | "test"; - versionAdded: number; -} - /** * File-wide logger. */ @@ -38,41 +31,6 @@ const logger = new Logger("preset-exchanges.ts"); const currentDefaultsVersion = 4; /** - * Exchanges that ship with wallet-core. - */ -const builtinExchanges: BuiltinExchange[] = [ - { - exchangeBaseUrl: "https://exchange.demo.taler.net/", - type: "demo", - currencyHint: "KUDOS", - versionAdded: 1, - }, - { - exchangeBaseUrl: "https://exchange.test.taler.net/", - type: "test", - currencyHint: "KUDOS", - versionAdded: 1, - }, - { - exchangeBaseUrl: "https://exchange.taler-ops.ch/", - currencyHint: "CHF", - type: "prod", - versionAdded: 3, - currencySpec: { - name: "Swiss francs", - common_amounts: ["CHF:5", "CHF:10", "CHF:25", "CHF:50"], - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": "Fr.", - "-2": "Rp.", - }, - }, - }, -]; - -/** * Insert the hard-coded defaults for exchanges, coins and * auditors into the database, unless these defaults have * already been applied. diff --git a/packages/taler-wallet-core/src/requests.test.ts b/packages/taler-wallet-core/src/requests.test.ts @@ -18,8 +18,11 @@ import assert from "node:assert"; import { test } from "node:test"; import { + ExchangeEntryStatus, ExchangeEntrySource, ExchangeRecommendationReason, + TalerError, + TalerErrorCode, TalerPreciseTimestamp, } from "@gnu-taler/taler-util"; @@ -117,7 +120,7 @@ function makeExchangeEntry( entryStatus: ExchangeEntryDbRecordStatus, options: { source?: ExchangeEntrySource; - presetType?: "prod" | "demo"; + presetType?: "prod" | "demo" | "test"; lastWithdrawalSeconds?: number; } = {}, ): WalletExchangeEntry { @@ -225,7 +228,9 @@ test("withdrawal candidates explain and rank recommendations", async () => { ), ]); - const result = await handleListWithdrawalExchangeCandidates(wex, {}); + const result = await handleListWithdrawalExchangeCandidates(wex, { + withBuiltin: false, + }); assert.deepStrictEqual( result.candidates.map((x) => x.exchangeBaseUrl), [ @@ -258,7 +263,9 @@ test("candidate reasons can report both preset and explicitly added", async () = }, ), ]); - const result = await handleListWithdrawalExchangeCandidates(wex, {}); + const result = await handleListWithdrawalExchangeCandidates(wex, { + withBuiltin: false, + }); assert.deepStrictEqual(result.candidates[0].recommendationReasons, [ ExchangeRecommendationReason.Preset, ExchangeRecommendationReason.UserAdded, @@ -286,10 +293,121 @@ test("explicitly adding an exchange records user provenance", async () => { assert.strictEqual(exchange.entryStatus, ExchangeEntryDbRecordStatus.Used); }); -test("presetOnly is shared with the deprecated default exchange request", async () => { +test("builtin exchange flags control catalog and persisted entries", async () => { + const emptyWex = makeExchangeTestContext([]); + const defaults = await handleListWithdrawalExchangeCandidates(emptyWex, {}); + assert.deepStrictEqual( + defaults.candidates.map((x) => x.exchangeBaseUrl), + ["https://exchange.taler-ops.ch/"], + ); + + const withDemo = await handleListWithdrawalExchangeCandidates(emptyWex, { + withDemo: true, + }); + assert.deepStrictEqual( + withDemo.candidates.map((x) => x.exchangeBaseUrl), + ["https://exchange.demo.taler.net/", "https://exchange.taler-ops.ch/"], + ); + + const withTest = await handleListWithdrawalExchangeCandidates(emptyWex, { + withTest: true, + }); + assert.deepStrictEqual( + withTest.candidates.map((x) => x.exchangeBaseUrl), + ["https://exchange.taler-ops.ch/", "https://exchange.test.taler.net/"], + ); + + const allBuiltins = await handleListWithdrawalExchangeCandidates(emptyWex, { + withDemo: true, + withTest: true, + }); + assert.deepStrictEqual( + allBuiltins.candidates.map((x) => x.exchangeBaseUrl), + [ + "https://exchange.demo.taler.net/", + "https://exchange.taler-ops.ch/", + "https://exchange.test.taler.net/", + ], + ); + + const persistedWex = makeExchangeTestContext([ + makeExchangeEntry( + "https://exchange.demo.taler.net/", + ExchangeEntryDbRecordStatus.Used, + { source: ExchangeEntrySource.Builtin, presetType: "demo" }, + ), + makeExchangeEntry( + "https://exchange.test.taler.net/", + ExchangeEntryDbRecordStatus.Used, + { source: ExchangeEntrySource.Builtin, presetType: "test" }, + ), + makeExchangeEntry( + "https://exchange.taler-ops.ch/", + ExchangeEntryDbRecordStatus.Used, + { source: ExchangeEntrySource.Builtin, presetType: "prod" }, + ), + ]); + const persistedProd = await handleListWithdrawalExchangeCandidates( + persistedWex, + { withBuiltin: false }, + ); + assert.deepStrictEqual( + persistedProd.candidates.map((x) => x.exchangeBaseUrl), + ["https://exchange.taler-ops.ch/"], + ); + assert.strictEqual( + persistedProd.candidates[0].exchangeEntryStatus, + ExchangeEntryStatus.Used, + ); + + const persistedNonProd = await handleListWithdrawalExchangeCandidates( + persistedWex, + { withBuiltin: false, withDemo: true, withTest: true }, + ); + assert.deepStrictEqual( + persistedNonProd.candidates.map((x) => x.exchangeBaseUrl), + [ + "https://exchange.demo.taler.net/", + "https://exchange.taler-ops.ch/", + "https://exchange.test.taler.net/", + ], + ); +}); + +test("persisted builtin candidate wins over the catalog duplicate", async () => { const wex = makeExchangeTestContext([ makeExchangeEntry( - "https://preset.example/", + "https://exchange.taler-ops.ch/", + ExchangeEntryDbRecordStatus.Used, + { source: ExchangeEntrySource.User, presetType: "prod" }, + ), + ]); + + const result = await handleListWithdrawalExchangeCandidates(wex, {}); + assert.strictEqual(result.candidates.length, 1); + assert.strictEqual(result.candidates[0].currency, "TESTKUDOS"); + assert.strictEqual( + result.candidates[0].exchangeEntryStatus, + ExchangeEntryStatus.Used, + ); + assert.deepStrictEqual(result.candidates[0].recommendationReasons, [ + ExchangeRecommendationReason.Preset, + ExchangeRecommendationReason.UserAdded, + ]); +}); + +test("presetOnly returns only production exchanges from the builtin list", async () => { + const wex = makeExchangeTestContext([ + makeExchangeEntry( + "https://exchange.taler-ops.ch/", + ExchangeEntryDbRecordStatus.Used, + { + source: ExchangeEntrySource.Builtin, + presetType: "prod", + }, + ), + makeExchangeEntry( + "https://custom-preset.example/", ExchangeEntryDbRecordStatus.Preset, { source: ExchangeEntrySource.Builtin, @@ -297,20 +415,27 @@ test("presetOnly is shared with the deprecated default exchange request", async }, ), makeExchangeEntry( - "https://used.example/", + "https://exchange.demo.taler.net/", ExchangeEntryDbRecordStatus.Used, { - source: ExchangeEntrySource.User, + source: ExchangeEntrySource.Builtin, + presetType: "demo", }, ), ]); const candidates = await handleListWithdrawalExchangeCandidates(wex, { presetOnly: true, + withDemo: true, + withTest: true, }); assert.deepStrictEqual( candidates.candidates.map((x) => x.exchangeBaseUrl), - ["https://preset.example/"], + ["https://exchange.taler-ops.ch/"], + ); + assert.strictEqual( + candidates.candidates[0].exchangeEntryStatus, + ExchangeEntryStatus.Used, ); let warning = ""; @@ -320,18 +445,10 @@ test("presetOnly is shared with the deprecated default exchange request", async return true; }) as typeof process.stderr.write; try { - const allLegacy = await handleGetDefaultExchanges(wex, {}); - assert.deepStrictEqual( - allLegacy.defaultExchanges.map((x) => x.talerUri), - [ - "taler://withdraw-exchange/preset.example/", - "taler://withdraw-exchange/used.example/", - ], - ); const legacy = await handleGetDefaultExchanges(wex, { presetOnly: true }); assert.deepStrictEqual( legacy.defaultExchanges.map((x) => x.talerUri), - ["taler://withdraw-exchange/preset.example/"], + ["taler://withdraw-exchange/exchange.taler-ops.ch/"], ); } finally { process.stderr.write = originalWrite; @@ -339,3 +456,23 @@ test("presetOnly is shared with the deprecated default exchange request", async assert.match(warning, /getDefaultExchanges is deprecated/); assert.match(warning, /listWithdrawalExchangeCandidates/); }); + +test("presetOnly rejects withBuiltin=false for both exchange APIs", async () => { + const wex = makeExchangeTestContext([]); + for (const operation of [ + handleListWithdrawalExchangeCandidates, + handleGetDefaultExchanges, + ]) { + await assert.rejects( + operation(wex, { presetOnly: true, withBuiltin: false }), + (error: unknown) => { + assert.ok(error instanceof TalerError); + assert.strictEqual( + error.errorDetail.code, + TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, + ); + return true; + }, + ); + } +}); diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts @@ -57,7 +57,6 @@ import { DeleteStoredBackupRequest, DeleteSubscriptionRequest, EmptyObject, - ExchangeEntryStatus, ExportDbToFileRequest, ExportDbToFileResponse, FailTransactionRequest, @@ -84,7 +83,6 @@ import { GetQrCodesForPaytoRequest, GetQrCodesForPaytoResponse, HintNetworkAvailabilityRequest, - HostPortPath, HttpStatusCode, ImportDbFromFileRequest, ImportDbRequest, @@ -2252,48 +2250,13 @@ export async function handleGetDefaultExchanges( logger.warn( "getDefaultExchanges is deprecated; use listWithdrawalExchangeCandidates instead", ); - 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) { - case ExchangeEntryStatus.Ephemeral: - continue; - } - if (exch.currency === "UNKNOWN") { - continue; - } - defaultExchanges.push({ - currency: exch.currency, - currencySpec: exch.currencySpec, - talerUri: TalerUris.stringify({ - type: TalerUriAction.WithdrawExchange, - exchangeBaseUrl: exch.exchangeBaseUrl as HostPortPath, - }), - }); - } - if (wex.ws.devExperimentState.fakeDefaultExchangeDemo) { - defaultExchanges.push({ - talerUri: "taler://withdraw-exchange/exchange.demo.taler.net/", - currency: "KUDOS", - currencySpec: { - name: "Kudos", - common_amounts: ["KUDOS:5", "KUDOS:10", "KUDOS:25", "KUDOS:50"], - num_fractional_input_digits: 2, - num_fractional_normal_digits: 2, - num_fractional_trailing_zero_digits: 2, - alt_unit_names: { - "0": "ク", - }, - }, - }); - } + const { candidates } = await listWithdrawalExchangeCandidates(wex, req); return { - defaultExchanges, + defaultExchanges: candidates.map((candidate) => ({ + currency: candidate.currency, + currencySpec: candidate.currencySpec, + talerUri: candidate.talerUri, + })), }; } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -1898,6 +1898,12 @@ export const walletApiExpectedErrors = { // --- Exchanges ---------------------------------------------------------- + [WalletApiOperation.GetDefaultExchanges]: [ + TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, + ], + [WalletApiOperation.ListWithdrawalExchangeCandidates]: [ + TalerErrorCode.WALLET_CORE_API_BAD_REQUEST, + ], [WalletApiOperation.AddExchange]: [ TalerErrorCode.WALLET_TALER_URI_MALFORMED, TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE,