taler-typescript-core

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

commit edcf051016a25c2daba531ad3b47e0772bf6660b
parent b32819d2d026822074c410be98ca97c7cd0226fa
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:42 +0200

wallet-core: spend only replacement payment coins

Diffstat:
Apackages/taler-wallet-core/src/pay-merchant.test.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/pay-merchant.ts | 20+++++++++++++++++---
2 files changed, 69 insertions(+), 3 deletions(-)

diff --git a/packages/taler-wallet-core/src/pay-merchant.test.ts b/packages/taler-wallet-core/src/pay-merchant.test.ts @@ -0,0 +1,52 @@ +/* + 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/> + */ +import { Amounts, AmountString, SelectedCoin } from "@gnu-taler/taler-util"; +import assert from "node:assert"; +import { test } from "node:test"; +import { getCoinsToSpendForMerchantRepair } from "./pay-merchant.js"; + +function makeSelectedCoin( + coinPub: string, + contribution: AmountString, +): SelectedCoin { + return { + coinPub, + contribution, + denomPubHash: `denom-${coinPub}`, + exchangeBaseUrl: "https://exchange.example/", + exchangeMasterPub: "exchange-master-pub", + }; +} + +test("merchant repair spends only newly selected coins", () => { + const retained = makeSelectedCoin( + "retained", + "TESTKUDOS:1" as AmountString, + ); + const added = makeSelectedCoin("added", "TESTKUDOS:2" as AmountString); + + const toSpend = getCoinsToSpendForMerchantRepair( + [ + { + coinPub: retained.coinPub, + contribution: Amounts.parseOrThrow(retained.contribution), + }, + ], + [retained, added], + ); + + assert.deepStrictEqual(toSpend, [added]); +}); diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -83,6 +83,7 @@ import { safeStringifyException, ScopeInfo, ScopeType, + SelectedCoin, SelectedProspectiveCoin, SharePaymentResult, SignedTokenEnvelope, @@ -110,6 +111,7 @@ import { throwUnexpectedRequestError, } from "@gnu-taler/taler-util/http"; import { + coinsAddedByRepair, PreviousPayCoins, selectPayCoins, selectPayCoinsInTx, @@ -1788,6 +1790,13 @@ function setCoinSel(rec: WalletPurchase, coinSel: PayCoinSelection): void { rec.exchanges.sort(); } +export function getCoinsToSpendForMerchantRepair( + previous: PreviousPayCoins, + selected: SelectedCoin[], +): SelectedCoin[] { + return coinsAddedByRepair(previous, selected); +} + async function reselectCoinsTx( tx: WalletDbTransaction, ctx: PayMerchantTransactionContext, @@ -1817,6 +1826,7 @@ async function reselectCoinsTx( const prevPayCoins: PreviousPayCoins = []; const prevTokensPubs: string[] = []; + let payCoinsToSpend: SelectedCoin[] = []; const payCoinSelection = p.payInfo.payCoinSelection; const payTokenSelection = p.payInfo.payTokenSelection; @@ -1861,6 +1871,10 @@ async function reselectCoinsTx( default: assertUnreachable(res); } + payCoinsToSpend = getCoinsToSpendForMerchantRepair( + prevPayCoins, + res.coinSel.coins, + ); setCoinSel(p, res.coinSel); } @@ -1900,9 +1914,9 @@ async function reselectCoinsTx( if (p.payInfo.payCoinSelection) { await spendCoins(ctx.wex, tx, { transactionId: ctx.transactionId, - coinPubs: p.payInfo.payCoinSelection.coinPubs, - contributions: p.payInfo.payCoinSelection.coinContributions.map((x) => - Amounts.parseOrThrow(x), + coinPubs: payCoinsToSpend.map((x) => x.coinPub), + contributions: payCoinsToSpend.map((x) => + Amounts.parseOrThrow(x.contribution), ), refreshReason: RefreshReason.PayMerchant, });