commit c70196ea62d4d5951455a14ab2740a76659ef522
parent db1c9223ceaf07d4d7e2542644679f626c523972
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:43 +0200
wallet-core: scope coin loss to exchange master key
Diffstat:
2 files changed, 64 insertions(+), 4 deletions(-)
diff --git a/packages/taler-wallet-core/src/exchanges.test.ts b/packages/taler-wallet-core/src/exchanges.test.ts
@@ -0,0 +1,45 @@
+/*
+ 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 assert from "node:assert";
+import { test } from "node:test";
+import { WalletCoin } from "./db-common.js";
+import { filterCoinsByExchangeMasterPub } from "./exchanges.js";
+
+test("denomination loss processing is scoped to one exchange master key", () => {
+ const current = {
+ coinPub: "current-coin",
+ denomPubHash: "shared-denomination-hash",
+ exchangeMasterPub: "current-master",
+ } as WalletCoin;
+ const previous = {
+ coinPub: "previous-coin",
+ denomPubHash: "shared-denomination-hash",
+ exchangeMasterPub: "previous-master",
+ } as WalletCoin;
+ const legacyOrphan = {
+ coinPub: "legacy-orphan",
+ denomPubHash: "shared-denomination-hash",
+ } as WalletCoin;
+
+ assert.deepStrictEqual(
+ filterCoinsByExchangeMasterPub(
+ [current, previous, legacyOrphan],
+ "current-master",
+ ),
+ [current],
+ );
+});
diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts
@@ -2879,6 +2879,15 @@ export async function processTaskExchangeAutoRefresh(
* /keys, and judging them against it would destroy the balance the wallet is
* meant to be preserving.
*/
+export function filterCoinsByExchangeMasterPub(
+ coins: WalletCoin[],
+ exchangeMasterPub: string,
+): WalletCoin[] {
+ return coins.filter(
+ (coin) => coin.exchangeMasterPub === exchangeMasterPub,
+ );
+}
+
async function handleDenomLoss(
wex: WalletExecutionContext,
tx: WalletDbTransaction,
@@ -2908,8 +2917,11 @@ async function handleDenomLoss(
denominations.map((denom) => [denomRefKey(denom), denom]),
);
const coinsByDenomHash = new Map<string, WalletCoin[]>();
- const affectedCoins = await tx.getCoinsByDenomPubHashes(
- coinAvailabilityRecs.map((availability) => availability.denomPubHash),
+ const affectedCoins = filterCoinsByExchangeMasterPub(
+ await tx.getCoinsByDenomPubHashes(
+ coinAvailabilityRecs.map((availability) => availability.denomPubHash),
+ ),
+ exchangeMasterPub,
);
for (const coin of affectedCoins) {
const group = coinsByDenomHash.get(coin.denomPubHash) ?? [];
@@ -3249,8 +3261,11 @@ async function handleRecoup(
denominations.map((denom) => [denomRefKey(denom), denom]),
);
const coinsByDenomHash = new Map<string, WalletCoin[]>();
- const affectedCoins = await tx.getCoinsByDenomPubHashes(
- recoupDenomList.map((recoupInfo) => recoupInfo.h_denom_pub),
+ const affectedCoins = filterCoinsByExchangeMasterPub(
+ await tx.getCoinsByDenomPubHashes(
+ recoupDenomList.map((recoupInfo) => recoupInfo.h_denom_pub),
+ ),
+ exchangeMasterPub,
);
for (const coin of affectedCoins) {
const group = coinsByDenomHash.get(coin.denomPubHash) ?? [];