diff options
Diffstat (limited to 'packages/taler-wallet-core/src/denomSelection.ts')
-rw-r--r-- | packages/taler-wallet-core/src/denomSelection.ts | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/denomSelection.ts b/packages/taler-wallet-core/src/denomSelection.ts new file mode 100644 index 000000000..ecc1fa881 --- /dev/null +++ b/packages/taler-wallet-core/src/denomSelection.ts @@ -0,0 +1,199 @@ +/* + 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 { + AbsoluteTime, + AmountJson, + Amounts, + DenomSelectionState, + ForcedDenomSel, + Logger, +} from "@gnu-taler/taler-util"; +import { DenominationRecord, timestampAbsoluteFromDb } 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); + let earliestDepositExpiration: AbsoluteTime | undefined; + let hasDenomWithAgeRestriction = false; + + denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate)); + denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); + + if (logger.shouldLogTrace()) { + logger.trace( + `selecting withdrawal denoms for ${Amounts.stringify(amountAvailable)}`, + ); + } + + 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, + }); + hasDenomWithAgeRestriction = + hasDenomWithAgeRestriction || d.denomPub.age_mask > 0; + const expireDeposit = timestampAbsoluteFromDb(d.stampExpireDeposit); + if (!earliestDepositExpiration) { + earliestDepositExpiration = expireDeposit; + } else { + earliestDepositExpiration = AbsoluteTime.min( + expireDeposit, + earliestDepositExpiration, + ); + } + } + + if (logger.shouldLogTrace()) { + logger.trace( + `denom_pub_hash=${ + d.denomPubHash + }, count=${count}, val=${Amounts.stringify( + d.value, + )}, wdfee=${Amounts.stringify(d.fees.feeWithdraw)}`, + ); + } + + if (Amounts.isZero(remaining)) { + break; + } + } + + if (logger.shouldLogTrace()) { + logger.trace("(end of denom selection)"); + } + + earliestDepositExpiration ??= AbsoluteTime.never(); + + return { + selectedDenoms, + totalCoinValue: Amounts.stringify(totalCoinValue), + totalWithdrawCost: Amounts.stringify(totalWithdrawCost), + earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp( + earliestDepositExpiration, + ), + hasDenomWithAgeRestriction, + }; +} + +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); + let earliestDepositExpiration: AbsoluteTime | undefined; + let hasDenomWithAgeRestriction = false; + + 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, + }); + hasDenomWithAgeRestriction = + hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0; + const expireDeposit = timestampAbsoluteFromDb(denom.stampExpireDeposit); + if (!earliestDepositExpiration) { + earliestDepositExpiration = expireDeposit; + } else { + earliestDepositExpiration = AbsoluteTime.min( + expireDeposit, + earliestDepositExpiration, + ); + } + } + + earliestDepositExpiration ??= AbsoluteTime.never(); + + return { + selectedDenoms, + totalCoinValue: Amounts.stringify(totalCoinValue), + totalWithdrawCost: Amounts.stringify(totalWithdrawCost), + earliestDepositExpiration: AbsoluteTime.toProtocolTimestamp( + earliestDepositExpiration, + ), + hasDenomWithAgeRestriction, + }; +} |