taler-typescript-core

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

commit 232c449c5686799b56df7363d8276384be523402
parent 6c7c66ff5cef6d88419298e6c945fd4a6d0f85ff
Author: Florian Dold <dold@taler.net>
Date:   Wed, 19 Aug 2026 18:17:58 +0200

wallet-core: report actual partial withdrawal amount

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

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-withdrawal-amount.ts | 28++++++++++++++++++++++++++--
Mpackages/taler-wallet-core/src/withdraw.ts | 28+++++++++++++++++++++++-----
2 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-amount.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-amount.ts @@ -21,9 +21,11 @@ import { AmountString, Logger, TalerWireGatewayHttpClient, + TransactionType, j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; @@ -36,7 +38,10 @@ export async function runWithdrawalAmountTest(t: GlobalTestState) { // Set up test environment const { walletClient, bankClient, exchange, exchangeBankAccount, bank } = - await createSimpleTestkudosEnvironmentV3(t); + await createSimpleTestkudosEnvironmentV3( + t, + makeNoFeeCoinConfig("TESTKUDOS"), + ); const wireGatewayApiClient = new TalerWireGatewayHttpClient( exchangeBankAccount.wireGatewayApiBaseUrl, @@ -65,6 +70,15 @@ export async function runWithdrawalAmountTest(t: GlobalTestState) { const reservePub: string = wres.reservePub; + const pendingTx = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: wres.transactionId, + }, + ); + t.assertTrue(pendingTx.type === TransactionType.Withdrawal); + t.assertAmountEquals(pendingTx.amountRaw, "TESTKUDOS:10"); + await wireGatewayApiClient.addIncoming({ auth: bank.getAdminAuth(), body: { @@ -83,7 +97,17 @@ export async function runWithdrawalAmountTest(t: GlobalTestState) { const balResp = await walletClient.call(WalletApiOperation.GetBalances, {}); // We managed to withdraw the actually transferred amount! - t.assertAmountEquals(balResp.balances[0].available, "TESTKUDOS:4.90"); + t.assertAmountEquals(balResp.balances[0].available, "TESTKUDOS:5"); + + const completedTx = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: wres.transactionId, + }, + ); + t.assertTrue(completedTx.type === TransactionType.Withdrawal); + t.assertAmountEquals(completedTx.amountRaw, "TESTKUDOS:5"); + t.assertAmountEquals(completedTx.amountEffective, "TESTKUDOS:5"); await t.shutdown(); } diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -215,6 +215,25 @@ interface TxKycDetails { kycPaytoHash?: string; } +/** + * Return the raw amount to expose for a withdrawal transaction. + * + * Before the reserve is funded, the instructed amount is what the user still + * needs to transfer. Once the reserve balance has been observed, denomination + * selection reflects what can actually be withdrawn from that balance. + */ +function getWithdrawalTransactionRawAmount( + wg: WalletWithdrawalGroup, + zero: AmountString, +): AmountString { + if (wg.reserveBalanceAmount !== undefined && wg.denomsSel !== undefined) { + return Amounts.stringify(wg.denomsSel.totalWithdrawCost); + } + return wg.instructedAmount === undefined + ? zero + : Amounts.stringify(wg.instructedAmount); +} + function buildTransactionForBankIntegratedWithdraw( wg: WalletWithdrawalGroup, scopes: ScopeInfo[], @@ -247,9 +266,7 @@ function buildTransactionForBankIntegratedWithdraw( isUnsuccessfulTransaction(txState) || !wg.denomsSel ? zero : Amounts.stringify(wg.denomsSel.totalCoinValue), - amountRaw: !wg.instructedAmount - ? zero - : Amounts.stringify(wg.instructedAmount), + amountRaw: getWithdrawalTransactionRawAmount(wg, zero), withdrawalDetails: { type: WithdrawalType.TalerBankIntegrationApi, confirmed: wg.wgInfo.bankInfo.timestampBankConfirmed ? true : false, @@ -309,6 +326,7 @@ function buildTransactionForManualWithdraw( ); const txState = computeWithdrawalTransactionStatus(wg); + const zero = Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)); let txDetails: TransactionWithdrawal = { type: TransactionType.Withdrawal, @@ -317,9 +335,9 @@ function buildTransactionForManualWithdraw( scopes, txActions: computeWithdrawalTransactionActions(wg), amountEffective: isUnsuccessfulTransaction(txState) - ? Amounts.stringify(Amounts.zeroOfAmount(wg.instructedAmount)) + ? zero : Amounts.stringify(wg.denomsSel.totalCoinValue), - amountRaw: Amounts.stringify(wg.instructedAmount), + amountRaw: getWithdrawalTransactionRawAmount(wg, zero), withdrawalDetails: { type: WithdrawalType.ManualTransfer, reservePub: wg.reservePub,