commit bec7ad84af7c5b343d078ea83d7db28f54b5d93c
parent 9be8e931ecba22572875a060f4d87db2527993fe
Author: Florian Dold <dold@taler.net>
Date: Tue, 1 Sep 2026 21:49:46 +0200
wallet-core: filter legacy exchange transactions by URL
Diffstat:
2 files changed, 35 insertions(+), 15 deletions(-)
diff --git a/packages/taler-wallet-core/src/transactions.test.ts b/packages/taler-wallet-core/src/transactions.test.ts
@@ -13,7 +13,7 @@
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 { TransactionType } from "@gnu-taler/taler-util";
+import { ScopeType, TransactionType } from "@gnu-taler/taler-util";
import assert from "node:assert";
import { test } from "node:test";
import {
@@ -22,6 +22,7 @@ import {
ParsedTransactionIdentifier,
parseTransactionIdentifier,
resolveTransactionReference,
+ shouldSkipCurrency,
} from "./transactions.js";
import type { WalletWithdrawalGroup } from "./db/records.js";
import type { WalletDbTransaction } from "./db/transaction.js";
@@ -131,3 +132,29 @@ test("an unknown or ambiguous bank withdrawal reference is not resolved", async
}),
);
});
+
+test("legacy exchange scope conservatively matches transactions by URL", () => {
+ const request = {
+ scopeInfo: {
+ type: ScopeType.ExchangeLegacyKeys as const,
+ currency: "TESTKUDOS",
+ url: "https://exchange/",
+ masterPub: "superseded-key",
+ },
+ };
+ assert.equal(
+ shouldSkipCurrency(request, "TESTKUDOS", ["https://exchange/"]),
+ false,
+ "the matching URL must be included regardless of the master key",
+ );
+ assert.equal(
+ shouldSkipCurrency(request, "TESTKUDOS", ["https://other-exchange/"]),
+ true,
+ "a different URL must still be excluded",
+ );
+ assert.equal(
+ shouldSkipCurrency(request, "OTHER", ["https://exchange/"]),
+ true,
+ "a different currency must still be excluded",
+ );
+});
diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts
@@ -77,7 +77,7 @@ import { WalletDbTransaction } from "./db/transaction.js";
const logger = new Logger("taler-wallet-core:transactions.ts");
-function shouldSkipCurrency(
+export function shouldSkipCurrency(
transactionsRequest:
| TransactionsRequest
| GetTransactionsV2Request
@@ -94,7 +94,12 @@ function shouldSkipCurrency(
case ScopeType.Global: {
return !sameCurrency;
}
- case ScopeType.Exchange: {
+ case ScopeType.Exchange:
+ case ScopeType.ExchangeLegacyKeys: {
+ // Transaction metadata only records the exchange URL. Treat every
+ // key set served at that URL as a conservative match. This can
+ // return false positives after a key rotation, but never hides older
+ // transactions whose exact key set was not recorded.
return (
!sameCurrency ||
(exchangesInTransaction.length > 0 &&
@@ -109,18 +114,6 @@ function shouldSkipCurrency(
"filtering transactions by auditor scope is not implemented",
);
}
- case ScopeType.ExchangeLegacyKeys: {
- // A transaction records the exchanges it involved by URL, which does
- // not say which key set signed the coins, so this filter cannot be
- // answered accurately. Refusing beats quietly returning the
- // transactions of the exchange's current key set as if they were the
- // superseded ones.
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_CORE_API_BAD_REQUEST,
- { parameter: "scopeInfo" },
- "filtering transactions by a superseded exchange key set is not implemented",
- );
- }
default:
assertUnreachable(transactionsRequest.scopeInfo);
}