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:
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,