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:
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,