/* 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 */ /** * 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, }; }