commit 2522ef5c322eacd14e458080369c349a7d72f66e
parent 3acb3815ffb134e8be68c837620c6c53080e33fe
Author: Florian Dold <dold@taler.net>
Date: Thu, 27 Aug 2026 18:52:06 +0200
wallet-core: skip malformed exchange accounts
Diffstat:
7 files changed, 136 insertions(+), 21 deletions(-)
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
@@ -73,9 +73,7 @@ import {
GetBalanceDetailRequest,
j2s,
Logger,
- Paytos,
RefreshReason,
- Result,
ScopeInfo,
ScopeType,
} from "@gnu-taler/taler-util";
@@ -103,6 +101,7 @@ import {
import { getEffectiveExchangeType } from "./builtin-exchanges.js";
import { hasVerifiedAuditorTrust } from "./auditorTrust.js";
import { WalletDbTransaction } from "./db/transaction.js";
+import { parseExchangeWireAccountPayto } from "./exchange-payto.js";
import {
denomRefKey,
getDenomInfos,
@@ -1250,7 +1249,7 @@ export async function getBalanceDetail(
continue;
}
details.wireInfo.accounts.forEach((a) => {
- const payto = Result.orUndefined(Paytos.fromString(a.payto_uri));
+ const payto = parseExchangeWireAccountPayto(a, e.baseUrl);
if (payto && !wires.includes(payto.targetType!)) {
wires.push(payto.targetType!);
}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -1535,6 +1535,34 @@ test("wire matching accepts a later unrestricted account", (t) => {
assert.strictEqual(res.status, "match");
});
+test("wire matching ignores a malformed exchange account", () => {
+ const wire = wireInfoWithRestrictions([]);
+ wire.wireInfo.accounts.unshift({
+ payto_uri: "payto://iban/DE4259845121444?receiver-name=exchange",
+ master_sig: "DUMMY",
+ credit_restrictions: [],
+ debit_restrictions: [],
+ });
+ const res = findMatchingWire(
+ "iban",
+ "payto://iban/CH62414246VCSW2LM4FG0",
+ wire,
+ );
+ assert.strictEqual(res.status, "match");
+});
+
+test("a malformed exchange account does not provide wire support", () => {
+ const wire = wireInfoWithRestrictions([]);
+ wire.wireInfo.accounts[0].payto_uri =
+ "payto://iban/DE4259845121444?receiver-name=exchange";
+ const res = findMatchingWire(
+ "iban",
+ "payto://iban/CH62414246VCSW2LM4FG0",
+ wire,
+ );
+ assert.strictEqual(res.status, "wire-method-unsupported");
+});
+
test("receiver acceptance identifies a master key mismatch at the same URL", () => {
const acceptance = checkExchangeAccepted(
{
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -59,7 +59,6 @@ import {
PaymentInsufficientBalanceDetails,
PaymentInsufficientBalanceStructuredDetails,
Paytos,
- Result,
ScopeInfo,
ScopeType,
SelectedCoin,
@@ -73,6 +72,7 @@ import {
PaymentBalanceDetails,
} from "./balance.js";
import { getAutoRefreshExecuteThreshold } from "./common.js";
+import { parseExchangeWireAccountPayto } from "./exchange-payto.js";
import {
DenominationVerificationStatus,
WalletDenomination,
@@ -1832,12 +1832,10 @@ export function findMatchingWire(
)?.wireFee;
for (const acc of exchangeWireDetails.wireInfo.accounts) {
- const ppRes = Paytos.fromString(acc.payto_uri);
- if (Result.isError(ppRes)) {
- throw Error(`failed to parse payto ${acc.payto_uri}`);
+ const pp = parseExchangeWireAccountPayto(acc);
+ if (!pp) {
+ continue;
}
- const pp = ppRes.value;
- checkLogicInvariant(!!pp);
if (pp.targetType !== wireMethod) {
continue;
}
diff --git a/packages/taler-wallet-core/src/dbless.ts b/packages/taler-wallet-core/src/dbless.ts
@@ -60,6 +60,7 @@ import {
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { ExchangeInfo, downloadExchangeInfo } from "./exchanges.js";
+import { filterValidExchangeWireAccounts } from "./exchange-payto.js";
import { getBankWithdrawalInfo } from "./withdraw.js";
export { downloadExchangeInfo };
@@ -109,8 +110,10 @@ export async function topupReserveWithBank(args: TopupReserveWithBankArgs) {
if (!bankInfo.exchange) {
throw Error("no suggested exchange");
}
- const plainPaytoUris =
- exchangeInfo.keys.accounts.map((x) => x.payto_uri) ?? [];
+ const plainPaytoUris = filterValidExchangeWireAccounts(
+ exchangeInfo.keys.accounts,
+ exchangeInfo.keys.base_url,
+ ).map((x) => x.payto_uri);
if (plainPaytoUris.length <= 0) {
throw new Error();
}
@@ -483,11 +486,15 @@ export async function createTestingReserve(args: {
exchangeInfo: ExchangeInfo;
}): Promise<void> {
const { http, corebankApiBaseUrl, amount, reservePub } = args;
- const paytoUri = args.exchangeInfo.keys.accounts[0].payto_uri;
- const pt = Result.orUndefined(Paytos.fromString(paytoUri));
- if (!pt) {
+ const account = filterValidExchangeWireAccounts(
+ args.exchangeInfo.keys.accounts,
+ args.exchangeInfo.keys.base_url,
+ )[0];
+ if (!account) {
throw Error("failed to parse payto URI");
}
+ const paytoUri = account.payto_uri;
+ const pt = Result.unpack(Paytos.fromString(paytoUri));
const components = pt.fullPath.split("/");
const creditorAcct = components[components.length - 1];
const wireGatewayClient = new TalerWireGatewayHttpClient(
diff --git a/packages/taler-wallet-core/src/exchange-payto.ts b/packages/taler-wallet-core/src/exchange-payto.ts
@@ -0,0 +1,57 @@
+/*
+ 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 <http://www.gnu.org/licenses/>.
+*/
+
+import {
+ ExchangeWireAccount,
+ Logger,
+ Paytos,
+ Result,
+ j2s,
+} from "@gnu-taler/taler-util";
+
+const logger = new Logger("exchange-payto.ts");
+
+/**
+ * Parse an exchange's wire account, ignoring malformed accounts.
+ *
+ * Exchange account data can outlive the wallet-core version that originally
+ * accepted it. Keep this check at the use sites as well as at /keys import so
+ * stricter payto validation cannot make an old database unusable.
+ */
+export function parseExchangeWireAccountPayto(
+ account: ExchangeWireAccount,
+ exchangeBaseUrl?: string,
+): Paytos.URI | undefined {
+ const result = Paytos.fromString(account.payto_uri);
+ if (Result.isError(result)) {
+ const exchange = exchangeBaseUrl ? ` from ${exchangeBaseUrl}` : "";
+ logger.warn(
+ `ignoring malformed exchange account${exchange}: ${j2s(account.payto_uri)}`,
+ );
+ return undefined;
+ }
+ return result.value;
+}
+
+export function filterValidExchangeWireAccounts(
+ accounts: ExchangeWireAccount[],
+ exchangeBaseUrl?: string,
+): ExchangeWireAccount[] {
+ return accounts.filter(
+ (account) =>
+ parseExchangeWireAccountPayto(account, exchangeBaseUrl) !== undefined,
+ );
+}
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -207,6 +207,10 @@ import {
WithdrawTransactionContext,
updateWithdrawalDenomsForExchange,
} from "./withdraw.js";
+import {
+ filterValidExchangeWireAccounts,
+ parseExchangeWireAccountPayto,
+} from "./exchange-payto.js";
const logger = new Logger("exchanges.ts");
@@ -269,8 +273,22 @@ async function getExchangeRecordsInternal(
logger.warn(
`no exchange details with pointer ${j2s(dp)} for ${exchangeBaseUrl}`,
);
+ return undefined;
+ }
+ const accounts = filterValidExchangeWireAccounts(
+ details.wireInfo.accounts,
+ exchangeBaseUrl,
+ );
+ if (accounts.length === details.wireInfo.accounts.length) {
+ return details;
}
- return details;
+ return {
+ ...details,
+ wireInfo: {
+ ...details.wireInfo,
+ accounts,
+ },
+ };
}
/**
@@ -913,7 +931,11 @@ async function validateWireInfo(
wireInfo: ExchangeKeysResponse,
masterPublicKey: string,
): Promise<WireInfo> {
+ const accounts: ExchangeWireAccount[] = [];
for (const a of wireInfo.accounts) {
+ if (!parseExchangeWireAccountPayto(a, wireInfo.base_url)) {
+ continue;
+ }
logger.trace("validating exchange acct");
let isValid = false;
if (wex.ws.config.testing.insecureTrustExchange) {
@@ -931,6 +953,7 @@ async function validateWireInfo(
`signature of exchange account ${a.payto_uri} is invalid`,
);
}
+ accounts.push(a);
}
logger.trace("account validation done");
const feesForType: WireFeeMap = {};
@@ -970,7 +993,7 @@ async function validateWireInfo(
}
return {
- accounts: wireInfo.accounts,
+ accounts,
feesForType,
};
}
@@ -3603,7 +3626,7 @@ export async function getExchangePaytoUri(
});
const accounts = details?.wireInfo.accounts ?? [];
for (const account of accounts) {
- const res = Result.orUndefined(Paytos.fromString(account.payto_uri));
+ const res = parseExchangeWireAccountPayto(account, exchangeBaseUrl);
if (!res) {
continue;
}
diff --git a/packages/taler-wallet-core/src/requests.ts b/packages/taler-wallet-core/src/requests.ts
@@ -458,6 +458,7 @@ import {
rematerializeTransactionsAtCurrentVersion,
walletExchangeClient,
} from "./wallet.js";
+import { parseExchangeWireAccountPayto } from "./exchange-payto.js";
const logger = new Logger("requests.ts");
@@ -1546,8 +1547,9 @@ async function handleGetDepositWireTypes(
if (!usable) {
continue;
}
- const parsedPayto = Result.orUndefined(
- Paytos.fromString(acc.payto_uri),
+ const parsedPayto = parseExchangeWireAccountPayto(
+ acc,
+ exchange.baseUrl,
);
if (!parsedPayto) {
continue;
@@ -1615,8 +1617,9 @@ async function handleGetDepositWireTypesForCurrency(
if (!usable) {
continue;
}
- const parsedPayto = Result.orUndefined(
- Paytos.fromString(acc.payto_uri),
+ const parsedPayto = parseExchangeWireAccountPayto(
+ acc,
+ exchange.baseUrl,
);
if (!parsedPayto) {
continue;