taler-typescript-core

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

commit 9439f39b0138a89839b4eb22828cc8a965d66c42
parent 0e5b7484508d8a32207014b5d9a550439eaefa6f
Author: Florian Dold <dold@taler.net>
Date:   Sat,  5 Sep 2026 13:45:00 +0200

taler-harness: check final amounts after transaction recovery

Compare final amounts with actual balance changes after payment and P2P
aborts, purse expiry, deposit recovery and incremental refunds. Verify that
settled amounts survive deleting refresh records and restarting the wallet.

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

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts | 11+++++++++++
Mpackages/taler-harness/src/integrationtests/test-payment-abort.ts | 27++++++++++++++++++++++++++-
Mpackages/taler-harness/src/integrationtests/test-peer-abort-balance.ts | 6++++++
Mpackages/taler-harness/src/integrationtests/test-refund-incremental.ts | 21+++++++++++++++++++--
Apackages/taler-harness/src/integrationtests/test-transaction-final-amounts.ts | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
6 files changed, 202 insertions(+), 3 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts b/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts @@ -133,6 +133,10 @@ export async function runKycWalletDepositAbortTest(t: GlobalTestState) { t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:0"); } + const quoted = await walletClient.call( + WalletApiOperation.GetTransactionById, + { transactionId: depositResp.transactionId }, + ); await walletClient.call(WalletApiOperation.AbortTransaction, { transactionId: depositResp.transactionId, }); @@ -154,6 +158,13 @@ export async function runKycWalletDepositAbortTest(t: GlobalTestState) { // The deposit selection and its abort each refresh change for 0.15. The // initial withdrawal made 19.84 available, so aborting restores 19.54. t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:19.54"); + const settled = await walletClient.call( + WalletApiOperation.GetTransactionById, + { transactionId: depositResp.transactionId }, + ); + t.assertAmountEquals(settled.amountEffective, quoted.amountEffective); + t.assertTrue(settled.amountEffectiveFinal !== undefined); + t.assertAmountEquals(settled.amountEffectiveFinal!, "TESTKUDOS:0.3"); } runKycWalletDepositAbortTest.suites = ["wallet", "kyc"]; diff --git a/packages/taler-harness/src/integrationtests/test-payment-abort.ts b/packages/taler-harness/src/integrationtests/test-payment-abort.ts @@ -19,6 +19,7 @@ */ import { ConfirmPayResultType, + Amounts, TalerCorebankApiClient, TalerErrorCode, TalerErrorDetail, @@ -61,6 +62,7 @@ export async function runPaymentAbortTest(t: GlobalTestState) { }); await wres.withdrawalFinishedCond; + const before = await walletClient.call(WalletApiOperation.GetBalances, {}); const merchantClient = new TalerMerchantInstanceHttpClient( faultyMerchant.makeInstanceBaseUrl(), @@ -187,7 +189,30 @@ export async function runPaymentAbortTest(t: GlobalTestState) { // where only one of them works. // FIXME: also check extended transaction list for refresh. - // FIXME: also check balance + const after = await walletClient.call(WalletApiOperation.GetBalances, {}); + const payment = txns2.transactions[1]; + t.assertAmountEquals( + payment.amountEffective, + txns.transactions[1].amountEffective, + ); + t.assertTrue(payment.amountEffectiveFinal !== undefined); + t.assertAmountEquals( + payment.amountEffectiveFinal!, + Amounts.stringify( + Amounts.sub(before.balances[0].available, after.balances[0].available) + .amount, + ), + ); + const detail = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: payment.transactionId, + }, + ); + t.assertAmountEquals( + detail.amountEffectiveFinal!, + payment.amountEffectiveFinal!, + ); } runPaymentAbortTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-peer-abort-balance.ts b/packages/taler-harness/src/integrationtests/test-peer-abort-balance.ts @@ -207,6 +207,12 @@ export async function runPeerAbortBalanceTest(t: GlobalTestState) { // refresh that the abort started has finished. await wallet1.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + const settled = await wallet1.call(WalletApiOperation.GetTransactionById, { + transactionId: tx.transactionId, + }); + t.assertAmountEquals(settled.amountEffective, tx.amountEffective); + t.assertTrue(settled.amountEffectiveFinal !== undefined); + t.assertAmountEquals(settled.amountEffectiveFinal!, zero); await checkBalance(wallet1, "sender after the push debit was aborted", { available: initialAmount, }); diff --git a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts @@ -116,6 +116,12 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { t.assertTrue(orderStatus.order_status === "paid"); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + const beforeRefunds = await walletClient.call( + WalletApiOperation.GetBalances, + {}, + ); + let ref = succeedOrThrow( await merchantClient.addRefund( merchantAdminAccessToken, @@ -233,10 +239,21 @@ export async function runRefundIncrementalTest(t: GlobalTestState) { const effective = Amounts.sum( txs.transactions .filter((x) => x.type === TransactionType.Refund) - .map((x) => x.amountEffective), + .map((x) => { + t.assertTrue(x.amountEffectiveFinal !== undefined); + return x.amountEffectiveFinal!; + }), ).amount; - t.assertAmountEquals("TESTKUDOS:9.66", effective); + t.assertAmountEquals( + Amounts.stringify( + Amounts.sub( + bal.balances[0].available, + beforeRefunds.balances[0].available, + ).amount, + ), + effective, + ); } await t.shutdown(); diff --git a/packages/taler-harness/src/integrationtests/test-transaction-final-amounts.ts b/packages/taler-harness/src/integrationtests/test-transaction-final-amounts.ts @@ -0,0 +1,138 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + SPDX-License-Identifier: GPL-3.0-or-later + */ +import { + AbsoluteTime, + Amounts, + Duration, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createSimpleTestkudosEnvironmentV3, + createWalletDaemonWithClient, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +/** Fee-bearing push cancellation and expiry must match the real balance loss. */ +export async function runTransactionFinalAmountsTest(t: GlobalTestState) { + const env = await createSimpleTestkudosEnvironmentV3(t); + const { bankClient, exchange } = env; + let { walletClient: wallet, walletService } = env; + const w = await withdrawViaBankV3(t, { + walletClient: wallet, + bankClient, + exchange, + amount: "TESTKUDOS:40", + }); + await w.withdrawalFinishedCond; + for (const outcome of ["abort", "expire"] as const) { + const before = await wallet.call(WalletApiOperation.GetBalances, {}); + const initiated = await wallet.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + amount: "TESTKUDOS:5", + summary: outcome, + purse_expiration: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ minutes: 2 }), + ), + ), + }, + }, + ); + await wallet.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: initiated.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }, + }); + const quoted = await wallet.call(WalletApiOperation.GetTransactionById, { + transactionId: initiated.transactionId, + }); + t.assertTrue(quoted.amountEffectiveFinal === undefined); + if (outcome === "abort") { + await wallet.call(WalletApiOperation.AbortTransaction, { + transactionId: initiated.transactionId, + }); + } else { + const offsetMs = Duration.toMilliseconds( + Duration.fromSpec({ minutes: 5 }), + ); + await exchange.stop(); + exchange.setTimetravel(offsetMs); + await exchange.start(); + await exchange.runExpireOnce(); + await wallet.call(WalletApiOperation.TestingSetTimetravel, { offsetMs }); + } + await wallet.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: initiated.transactionId, + txState: { + major: + outcome === "abort" + ? TransactionMajorState.Aborted + : TransactionMajorState.Expired, + }, + }); + await wallet.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + const after = await wallet.call(WalletApiOperation.GetBalances, {}); + const settled = await wallet.call(WalletApiOperation.GetTransactionById, { + transactionId: initiated.transactionId, + }); + t.assertTrue(settled.amountEffectiveFinal !== undefined); + t.assertAmountEquals(settled.amountEffective, quoted.amountEffective); + t.assertAmountEquals( + settled.amountEffectiveFinal!, + Amounts.stringify( + Amounts.sub(before.balances[0].available, after.balances[0].available) + .amount, + ), + ); + t.assertTrue(Amounts.isNonZero(settled.amountEffectiveFinal!)); + const listed = await wallet.call(WalletApiOperation.GetTransactionsV2, { + includeAll: true, + }); + t.assertAmountEquals( + listed.transactions.find( + (x) => x.transactionId === settled.transactionId, + )!.amountEffectiveFinal!, + settled.amountEffectiveFinal!, + ); + if (outcome === "abort") { + for (const child of listed.transactions) { + if ( + child.type === TransactionType.Refresh && + child.originatingTransactionId === settled.transactionId + ) { + await wallet.call(WalletApiOperation.DeleteTransaction, { + transactionId: child.transactionId, + }); + } + } + await walletService.stop(); + ({ walletClient: wallet, walletService } = + await createWalletDaemonWithClient(t, { + name: "wallet", + persistent: true, + })); + const restored = await wallet.call( + WalletApiOperation.GetTransactionById, + { transactionId: settled.transactionId }, + ); + t.assertAmountEquals( + restored.amountEffectiveFinal!, + settled.amountEffectiveFinal!, + ); + t.assertAmountEquals(restored.amountEffective, quoted.amountEffective); + } + } +} +runTransactionFinalAmountsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { runTransactionFinalAmountsTest } from "./test-transaction-final-amounts.js"; import { CancellationToken, Logger, @@ -352,6 +353,7 @@ const allTests: TestMainFunction[] = [ runWalletBalanceTest, runPaywallFlowTest, runPeerAbortBalanceTest, + runTransactionFinalAmountsTest, runPeerPullDebitPurseGoneTest, runPeerPullTest, runPeerPushTest,