diff options
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.ts | 205 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/denomSelection.ts | 150 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/refresh.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.test.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 2 |
5 files changed, 205 insertions, 156 deletions
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index c44ca3d17..1208e7c37 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021 Taler Systems S.A. + (C) 2021-2024 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 @@ -39,10 +39,8 @@ import { CoinPublicKeyString, CoinStatus, DenominationInfo, - DenomSelectionState, Duration, ForcedCoinSel, - ForcedDenomSel, InternationalizedString, j2s, Logger, @@ -59,7 +57,6 @@ import { } from "./balance.js"; import { getAutoRefreshExecuteThreshold } from "./common.js"; import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js"; -import { isWithdrawableDenom } from "./denominations.js"; import { ExchangeWireDetails, getExchangeWireDetailsInTx, @@ -292,49 +289,65 @@ export async function selectPayCoins( } satisfies SelectPayCoinsResult; } - const finalSel = selectedDenom; - - logger.trace(`coin selection request ${j2s(req)}`); - logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`); - - for (const dph of Object.keys(finalSel)) { - const selInfo = finalSel[dph]; - const numRequested = selInfo.contributions.length; - const query = [ - selInfo.exchangeBaseUrl, - selInfo.denomPubHash, - selInfo.maxAge, - CoinStatus.Fresh, - ]; - logger.trace(`query: ${j2s(query)}`); - const coins = - await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll( - query, - numRequested, - ); - if (coins.length != numRequested) { - throw Error( - `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`, - ); - } - coinPubs.push(...coins.map((x) => x.coinPub)); - coinContributions.push(...selInfo.contributions); - } + const coinSel = await assembleSelectPayCoinsSuccessResult( + tx, + selectedDenom, + coinPubs, + coinContributions, + req.contractTermsAmount, + tally, + ); return { type: "success", - coinSel: { - paymentAmount: Amounts.stringify(contractTermsAmount), - coinContributions: coinContributions.map((x) => Amounts.stringify(x)), - coinPubs, - customerDepositFees: Amounts.stringify(tally.customerDepositFees), - customerWireFees: Amounts.stringify(tally.customerWireFees), - }, + coinSel, }; }, ); } +async function assembleSelectPayCoinsSuccessResult( + tx: WalletDbReadOnlyTransaction<["coins"]>, + finalSel: SelResult, + coinPubs: string[], + coinContributions: AmountJson[], + contractTermsAmount: AmountJson, + tally: CoinSelectionTally, +): Promise<PayCoinSelection> { + for (const dph of Object.keys(finalSel)) { + const selInfo = finalSel[dph]; + const numRequested = selInfo.contributions.length; + const query = [ + selInfo.exchangeBaseUrl, + selInfo.denomPubHash, + selInfo.maxAge, + CoinStatus.Fresh, + ]; + logger.trace(`query: ${j2s(query)}`); + const coins = + await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll( + query, + numRequested, + ); + if (coins.length != numRequested) { + throw Error( + `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`, + ); + } + coinPubs.push(...coins.map((x) => x.coinPub)); + coinContributions.push(...selInfo.contributions); + } + + return { + // FIXME: Why do we return this?! + paymentAmount: Amounts.stringify(contractTermsAmount), + coinContributions: coinContributions.map((x) => Amounts.stringify(x)), + coinPubs, + customerDepositFees: Amounts.stringify(tally.customerDepositFees), + customerWireFees: Amounts.stringify(tally.customerWireFees), + }; +} + interface ReportInsufficientBalanceRequest { instructedAmount: AmountJson; requiredMinimumAge: number | undefined; @@ -783,120 +796,6 @@ async function selectPayCandidates( return [denoms, wfPerExchange]; } -/** - * Get a list of denominations (with repetitions possible) - * whose total value is as close as possible to the available - * amount, but never larger. - */ -export function selectWithdrawalDenominations( - amountAvailable: AmountJson, - denoms: DenominationRecord[], - denomselAllowLate: boolean = false, -): DenomSelectionState { - let remaining = Amounts.copy(amountAvailable); - - const selectedDenoms: { - count: number; - denomPubHash: string; - }[] = []; - - let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); - let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); - - denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); - denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); - - for (const d of denoms) { - const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount; - const res = Amounts.divmod(remaining, cost); - const count = res.quotient; - remaining = Amounts.sub(remaining, Amounts.mult(cost, count).amount).amount; - if (count > 0) { - totalCoinValue = Amounts.add( - totalCoinValue, - Amounts.mult(d.value, count).amount, - ).amount; - totalWithdrawCost = Amounts.add( - totalWithdrawCost, - Amounts.mult(cost, count).amount, - ).amount; - selectedDenoms.push({ - count, - denomPubHash: d.denomPubHash, - }); - } - - if (Amounts.isZero(remaining)) { - break; - } - } - - if (logger.shouldLogTrace()) { - logger.trace( - `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`, - ); - for (const sd of selectedDenoms) { - logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`); - } - logger.trace("(end of withdrawal denom list)"); - } - - return { - selectedDenoms, - totalCoinValue: Amounts.stringify(totalCoinValue), - totalWithdrawCost: Amounts.stringify(totalWithdrawCost), - }; -} - -export function selectForcedWithdrawalDenominations( - amountAvailable: AmountJson, - denoms: DenominationRecord[], - forcedDenomSel: ForcedDenomSel, - denomselAllowLate: boolean, -): DenomSelectionState { - const selectedDenoms: { - count: number; - denomPubHash: string; - }[] = []; - - let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); - let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); - - denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); - denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); - - for (const fds of forcedDenomSel.denoms) { - const count = fds.count; - const denom = denoms.find((x) => { - return Amounts.cmp(x.value, fds.value) == 0; - }); - if (!denom) { - throw Error( - `unable to find denom for forced selection (value ${fds.value})`, - ); - } - const cost = Amounts.add(denom.value, denom.fees.feeWithdraw).amount; - totalCoinValue = Amounts.add( - totalCoinValue, - Amounts.mult(denom.value, count).amount, - ).amount; - totalWithdrawCost = Amounts.add( - totalWithdrawCost, - Amounts.mult(cost, count).amount, - ).amount; - selectedDenoms.push({ - count, - denomPubHash: denom.denomPubHash, - }); - } - - return { - selectedDenoms, - totalCoinValue: Amounts.stringify(totalCoinValue), - totalWithdrawCost: Amounts.stringify(totalWithdrawCost), - }; -} - export interface CoinInfo { id: string; value: AmountJson; diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts new file mode 100644 index 000000000..12f8f8971 --- /dev/null +++ b/packages/taler-wallet-core/src/denomSelection.ts @@ -0,0 +1,150 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 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/> + */ + +/** + * Selection of denominations for withdrawals. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + AmountJson, + Amounts, + DenomSelectionState, + ForcedDenomSel, + Logger, +} from "@gnu-taler/taler-util"; +import { DenominationRecord } from "./db.js"; +import { isWithdrawableDenom } from "./denominations.js"; + +const logger = new Logger("denomSelection.ts"); + +/** + * Get a list of denominations (with repetitions possible) + * whose total value is as close as possible to the available + * amount, but never larger. + */ +export function selectWithdrawalDenominations( + amountAvailable: AmountJson, + denoms: DenominationRecord[], + denomselAllowLate: boolean = false, +): DenomSelectionState { + let remaining = Amounts.copy(amountAvailable); + + const selectedDenoms: { + count: number; + denomPubHash: string; + }[] = []; + + let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); + let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); + + denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + + for (const d of denoms) { + const cost = Amounts.add(d.value, d.fees.feeWithdraw).amount; + const res = Amounts.divmod(remaining, cost); + const count = res.quotient; + remaining = Amounts.sub(remaining, Amounts.mult(cost, count).amount).amount; + if (count > 0) { + totalCoinValue = Amounts.add( + totalCoinValue, + Amounts.mult(d.value, count).amount, + ).amount; + totalWithdrawCost = Amounts.add( + totalWithdrawCost, + Amounts.mult(cost, count).amount, + ).amount; + selectedDenoms.push({ + count, + denomPubHash: d.denomPubHash, + }); + } + + if (Amounts.isZero(remaining)) { + break; + } + } + + if (logger.shouldLogTrace()) { + logger.trace( + `selected withdrawal denoms for ${Amounts.stringify(totalCoinValue)}`, + ); + for (const sd of selectedDenoms) { + logger.trace(`denom_pub_hash=${sd.denomPubHash}, count=${sd.count}`); + } + logger.trace("(end of withdrawal denom list)"); + } + + return { + selectedDenoms, + totalCoinValue: Amounts.stringify(totalCoinValue), + totalWithdrawCost: Amounts.stringify(totalWithdrawCost), + }; +} + +export function selectForcedWithdrawalDenominations( + amountAvailable: AmountJson, + denoms: DenominationRecord[], + forcedDenomSel: ForcedDenomSel, + denomselAllowLate: boolean, +): DenomSelectionState { + const selectedDenoms: { + count: number; + denomPubHash: string; + }[] = []; + + let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency); + let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency); + + denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + + for (const fds of forcedDenomSel.denoms) { + const count = fds.count; + const denom = denoms.find((x) => { + return Amounts.cmp(x.value, fds.value) == 0; + }); + if (!denom) { + throw Error( + `unable to find denom for forced selection (value ${fds.value})`, + ); + } + const cost = Amounts.add(denom.value, denom.fees.feeWithdraw).amount; + totalCoinValue = Amounts.add( + totalCoinValue, + Amounts.mult(denom.value, count).amount, + ).amount; + totalWithdrawCost = Amounts.add( + totalWithdrawCost, + Amounts.mult(cost, count).amount, + ).amount; + selectedDenoms.push({ + count, + denomPubHash: denom.denomPubHash, + }); + } + + return { + selectedDenoms, + totalCoinValue: Amounts.stringify(totalCoinValue), + totalWithdrawCost: Amounts.stringify(totalWithdrawCost), + }; +} diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts index 9c272ad18..c6a7b768d 100644 --- a/packages/taler-wallet-core/src/refresh.ts +++ b/packages/taler-wallet-core/src/refresh.ts @@ -61,7 +61,7 @@ import { readSuccessResponseJsonOrThrow, readUnexpectedResponseDetails, } from "@gnu-taler/taler-util/http"; -import { selectWithdrawalDenominations } from "./coinSelection.js"; +import { selectWithdrawalDenominations } from "./denomSelection.js"; import { constructTaskIdentifier, makeCoinAvailable, diff --git a/packages/taler-wallet-core/src/withdraw.test.ts b/packages/taler-wallet-core/src/withdraw.test.ts index 3e92b1717..d8757d0cf 100644 --- a/packages/taler-wallet-core/src/withdraw.test.ts +++ b/packages/taler-wallet-core/src/withdraw.test.ts @@ -21,7 +21,7 @@ import { DenominationVerificationStatus, timestampProtocolToDb, } from "./db.js"; -import { selectWithdrawalDenominations } from "./coinSelection.js"; +import { selectWithdrawalDenominations } from "./denomSelection.js"; test("withdrawal selection bug repro", (t) => { const amount = { diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 853a5e0df..0f70479a5 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -97,7 +97,7 @@ import { import { selectForcedWithdrawalDenominations, selectWithdrawalDenominations, -} from "./coinSelection.js"; +} from "./denomSelection.js"; import { PendingTaskType, TaskIdStr, |