taler-typescript-core

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

commit cb98cc6ca5eb923de6551b203eee7e51b5b4fef9
parent ae44f3187c0428ff9c84c881e194df9f1642d06b
Author: Florian Dold <dold@taler.net>
Date:   Thu,  6 Aug 2026 19:02:48 +0200

harness: check which denominations of a /keys response get stored

Issue: https://bugs.taler.net/n/11334

Diffstat:
Apackages/taler-harness/src/integrationtests/test-exchange-denom-storage.ts | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 159 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-exchange-denom-storage.ts b/packages/taler-harness/src/integrationtests/test-exchange-denom-storage.ts @@ -0,0 +1,157 @@ +/* + 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/> + */ + +/** + * Imports. + */ +import { Duration, j2s } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + applyTimeTravelV2, + createSimpleTestkudosEnvironmentV3, + createWalletDaemonWithClient, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +/** + * Check which denominations of a /keys response the wallet writes to its + * database. + * + * An exchange keeps advertising a denomination long after it stops issuing + * it, so most of a /keys response is denominations no wallet can withdraw + * from any more. A wallet seeing them for the first time has no use for + * them; a wallet that holds coins of one needs it for as long as it holds + * them, and dropping it would count those coins as vanished. + */ +export async function runExchangeDenomStorageTest( + t: GlobalTestState, +): Promise<void> { + // Set up test environment + + const { walletClient, bankClient, exchange, merchant } = + await createSimpleTestkudosEnvironmentV3(t); + + // Withdraw digital cash into the wallet, so that it holds coins of the + // denominations the exchange currently issues. + + const wres = await withdrawViaBankV3(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + }); + await wres.withdrawalFinishedCond; + + const balanceBefore = await walletClient.call( + WalletApiOperation.GetBalances, + {}, + ); + + const statsFirstGen = await walletClient.call( + WalletApiOperation.TestingGetDenomStats, + { exchangeBaseUrl: exchange.baseUrl }, + ); + console.log(`stats of the first generation: ${j2s(statsFirstGen)}`); + t.assertTrue(statsFirstGen.numKnown > 0); + + t.logStep("withdrawal done"); + + // Move past the withdrawal expiration of those denominations. The exchange + // issues a second generation and keeps advertising the first, which stays + // depositable for two years. + + const travelMs = Duration.toMilliseconds(Duration.fromSpec({ days: 10 })); + await applyTimeTravelV2(travelMs, { exchange, merchant, walletClient }); + + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + exchangeBaseUrl: exchange.baseUrl, + force: true, + }); + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { + exchangeBaseUrl: exchange.baseUrl, + forceUpdate: true, + }); + + const statsBothGens = await walletClient.call( + WalletApiOperation.TestingGetDenomStats, + { exchangeBaseUrl: exchange.baseUrl }, + ); + console.log(`stats of both generations: ${j2s(statsBothGens)}`); + + // The first generation is not withdrawable any more, but this wallet holds + // coins of it: it keeps the denominations, still marked as offered, and + // writes nothing off. + const secondGenSize = statsBothGens.numKnown - statsFirstGen.numKnown; + t.assertTrue(secondGenSize > 0); + t.assertDeepEqual(statsBothGens.numOffered, statsBothGens.numKnown); + + const balanceAfter = await walletClient.call( + WalletApiOperation.GetBalances, + {}, + ); + t.assertDeepEqual(balanceAfter, balanceBefore); + + const txs = await walletClient.call(WalletApiOperation.GetTransactions, { + sort: "stable-ascending", + }); + for (const tx of txs.transactions) { + t.assertTrue(tx.type !== "denom-loss"); + } + + t.logStep("second generation known to the first wallet"); + + // A wallet meeting the exchange for the first time now gets the same /keys + // response, but has no coins of the first generation and no use for it. + + const { walletClient: freshWalletClient } = + await createWalletDaemonWithClient(t, { name: "wallet-fresh" }); + await freshWalletClient.call(WalletApiOperation.TestingSetTimetravel, { + offsetMs: travelMs, + }); + + await freshWalletClient.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + }); + await freshWalletClient.call(WalletApiOperation.TestingWaitExchangeReady, { + exchangeBaseUrl: exchange.baseUrl, + forceUpdate: true, + }); + + const statsFresh = await freshWalletClient.call( + WalletApiOperation.TestingGetDenomStats, + { exchangeBaseUrl: exchange.baseUrl }, + ); + console.log(`stats of the fresh wallet: ${j2s(statsFresh)}`); + + // Exactly the denominations that are still withdrawable, i.e. the ones the + // first wallet gained on top of the generation it had. + t.assertDeepEqual(statsFresh.numKnown, secondGenSize); + t.assertDeepEqual(statsFresh.numOffered, statsFresh.numKnown); + + t.logStep("fresh wallet stored only the withdrawable denominations"); + + // What it stored is enough to withdraw with. + const wres2 = await withdrawViaBankV3(t, { + walletClient: freshWalletClient, + bankClient, + exchange, + amount: "TESTKUDOS:10", + }); + await wres2.withdrawalFinishedCond; +} + +runExchangeDenomStorageTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -63,6 +63,7 @@ import { runDonauMinusTTest } from "./test-donau-minus-t.js"; import { runDonauMultiTest } from "./test-donau-multi.js"; import { runDonauTest } from "./test-donau.js"; import { runExchangeBaseUrlCompletionTest } from "./test-exchange-base-url-completion.js"; +import { runExchangeDenomStorageTest } from "./test-exchange-denom-storage.js"; import { runExchangeDepositTest } from "./test-exchange-deposit.js"; import { runExchangeEphemeralTest } from "./test-exchange-ephemeral.js"; import { runExchangeKeysCherrypickTest } from "./test-exchange-keys-cherrypick.js"; @@ -289,6 +290,7 @@ const allTests: TestMainFunction[] = [ runSimplePaymentTest, runExchangeManagementFaultTest, runExchangeKeysCherrypickTest, + runExchangeDenomStorageTest, runExchangeTimetravelTest, runFeeRegressionTest, runForcedSelectionTest,