taler-typescript-core

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

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:
Mpackages/taler-wallet-core/src/balance.ts | 5++---
Mpackages/taler-wallet-core/src/coinSelection.test.ts | 28++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/coinSelection.ts | 10++++------
Mpackages/taler-wallet-core/src/dbless.ts | 17++++++++++++-----
Apackages/taler-wallet-core/src/exchange-payto.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/exchanges.ts | 29++++++++++++++++++++++++++---
Mpackages/taler-wallet-core/src/requests.ts | 11+++++++----
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;