summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/denomSelection.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/denomSelection.ts')
-rw-r--r--packages/taler-wallet-core/src/denomSelection.ts199
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,
+ };
+}