summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/util/coinSelection.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/util/coinSelection.ts')
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts332
1 files changed, 0 insertions, 332 deletions
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
deleted file mode 100644
index 500cee5d8..000000000
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 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 coins for payments.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { strcmp, Logger } from "@gnu-taler/taler-util";
-
-const logger = new Logger("coinSelection.ts");
-
-/**
- * Result of selecting coins, contains the exchange, and selected
- * coins with their denomination.
- */
-export interface PayCoinSelection {
- /**
- * Amount requested by the merchant.
- */
- paymentAmount: AmountJson;
-
- /**
- * Public keys of the coins that were selected.
- */
- coinPubs: string[];
-
- /**
- * Amount that each coin contributes.
- */
- coinContributions: AmountJson[];
-
- /**
- * How much of the wire fees is the customer paying?
- */
- customerWireFees: AmountJson;
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- customerDepositFees: AmountJson;
-}
-
-/**
- * Structure to describe a coin that is available to be
- * used in a payment.
- */
-export interface AvailableCoinInfo {
- /**
- * Public key of the coin.
- */
- coinPub: string;
-
- /**
- * Coin's denomination public key.
- */
- denomPub: string;
-
- /**
- * Amount still remaining (typically the full amount,
- * as coins are always refreshed after use.)
- */
- availableAmount: AmountJson;
-
- /**
- * Deposit fee for the coin.
- */
- feeDeposit: AmountJson;
-
- exchangeBaseUrl: string;
-}
-
-export type PreviousPayCoins = {
- coinPub: string;
- contribution: AmountJson;
- feeDeposit: AmountJson;
- exchangeBaseUrl: string;
-}[];
-
-export interface CoinCandidateSelection {
- candidateCoins: AvailableCoinInfo[];
- wireFeesPerExchange: Record<string, AmountJson>;
-}
-
-export interface SelectPayCoinRequest {
- candidates: CoinCandidateSelection;
- contractTermsAmount: AmountJson;
- depositFeeLimit: AmountJson;
- wireFeeLimit: AmountJson;
- wireFeeAmortization: number;
- prevPayCoins?: PreviousPayCoins;
-}
-
-interface CoinSelectionTally {
- /**
- * Amount that still needs to be paid.
- * May increase during the computation when fees need to be covered.
- */
- amountPayRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards wire fees
- */
- amountWireFeeLimitRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards deposit fees
- * (and wire fees after wire fee limit is exhausted)
- */
- amountDepositFeeLimitRemaining: AmountJson;
-
- customerDepositFees: AmountJson;
-
- customerWireFees: AmountJson;
-
- wireFeeCoveredForExchange: Set<string>;
-}
-
-/**
- * Account for the fees of spending a coin.
- */
-function tallyFees(
- tally: CoinSelectionTally,
- wireFeesPerExchange: Record<string, AmountJson>,
- wireFeeAmortization: number,
- exchangeBaseUrl: string,
- feeDeposit: AmountJson,
-): CoinSelectionTally {
- const currency = tally.amountPayRemaining.currency;
- let amountWireFeeLimitRemaining = tally.amountWireFeeLimitRemaining;
- let amountDepositFeeLimitRemaining = tally.amountDepositFeeLimitRemaining;
- let customerDepositFees = tally.customerDepositFees;
- let customerWireFees = tally.customerWireFees;
- let amountPayRemaining = tally.amountPayRemaining;
- const wireFeeCoveredForExchange = new Set(tally.wireFeeCoveredForExchange);
-
- if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) {
- const wf =
- wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.getZero(currency);
- const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf);
- amountWireFeeLimitRemaining = Amounts.sub(
- amountWireFeeLimitRemaining,
- wfForgiven,
- ).amount;
- // The remaining, amortized amount needs to be paid by the
- // wallet or covered by the deposit fee allowance.
- let wfRemaining = Amounts.divide(
- Amounts.sub(wf, wfForgiven).amount,
- wireFeeAmortization,
- );
-
- // This is the amount forgiven via the deposit fee allowance.
- const wfDepositForgiven = Amounts.min(
- amountDepositFeeLimitRemaining,
- wfRemaining,
- );
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- wfDepositForgiven,
- ).amount;
-
- wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
- customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
- wireFeeCoveredForExchange.add(exchangeBaseUrl);
- }
-
- const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
-
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- dfForgiven,
- ).amount;
-
- // How much does the user spend on deposit fees for this coin?
- const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount;
- customerDepositFees = Amounts.add(customerDepositFees, dfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, dfRemaining).amount;
-
- return {
- amountDepositFeeLimitRemaining,
- amountPayRemaining,
- amountWireFeeLimitRemaining,
- customerDepositFees,
- customerWireFees,
- wireFeeCoveredForExchange,
- };
-}
-
-/**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
- *
- * The prevPayCoins can be specified to "repair" a coin selection
- * by adding additional coins, after a broken (e.g. double-spent) coin
- * has been removed from the selection.
- *
- * This function is only exported for the sake of unit tests.
- */
-export function selectPayCoins(
- req: SelectPayCoinRequest,
-): PayCoinSelection | undefined {
- const {
- candidates,
- contractTermsAmount,
- depositFeeLimit,
- wireFeeLimit,
- wireFeeAmortization,
- } = req;
-
- if (candidates.candidateCoins.length === 0) {
- return undefined;
- }
- const coinPubs: string[] = [];
- const coinContributions: AmountJson[] = [];
- const currency = contractTermsAmount.currency;
-
- let tally: CoinSelectionTally = {
- amountPayRemaining: contractTermsAmount,
- amountWireFeeLimitRemaining: wireFeeLimit,
- amountDepositFeeLimitRemaining: depositFeeLimit,
- customerDepositFees: Amounts.getZero(currency),
- customerWireFees: Amounts.getZero(currency),
- wireFeeCoveredForExchange: new Set(),
- };
-
- const prevPayCoins = req.prevPayCoins ?? [];
-
- // Look at existing pay coin selection and tally up
- for (const prev of prevPayCoins) {
- tally = tallyFees(
- tally,
- candidates.wireFeesPerExchange,
- wireFeeAmortization,
- prev.exchangeBaseUrl,
- prev.feeDeposit,
- );
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- prev.contribution,
- ).amount;
-
- coinPubs.push(prev.coinPub);
- coinContributions.push(prev.contribution);
- }
-
- const prevCoinPubs = new Set(prevPayCoins.map((x) => x.coinPub));
-
- // Sort by available amount (descending), deposit fee (ascending) and
- // denomPub (ascending) if deposit fee is the same
- // (to guarantee deterministic results)
- const candidateCoins = [...candidates.candidateCoins].sort(
- (o1, o2) =>
- -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
- Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPub, o2.denomPub),
- );
-
- // FIXME: Here, we should select coins in a smarter way.
- // Instead of always spending the next-largest coin,
- // we should try to find the smallest coin that covers the
- // amount.
-
- for (const aci of candidateCoins) {
- // Don't use this coin if depositing it is more expensive than
- // the amount it would give the merchant.
- if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) > 0) {
- continue;
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- // We have spent enough!
- break;
- }
-
- // The same coin can't contribute twice to the same payment,
- // by a fundamental, intentional limitation of the protocol.
- if (prevCoinPubs.has(aci.coinPub)) {
- continue;
- }
-
- tally = tallyFees(
- tally,
- candidates.wireFeesPerExchange,
- wireFeeAmortization,
- aci.exchangeBaseUrl,
- aci.feeDeposit,
- );
-
- let coinSpend = Amounts.max(
- Amounts.min(tally.amountPayRemaining, aci.availableAmount),
- aci.feeDeposit,
- );
-
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- coinSpend,
- ).amount;
- coinPubs.push(aci.coinPub);
- coinContributions.push(coinSpend);
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- return {
- paymentAmount: contractTermsAmount,
- coinContributions,
- coinPubs,
- customerDepositFees: tally.customerDepositFees,
- customerWireFees: tally.customerWireFees,
- };
- }
- return undefined;
-}