wallet-core/packages/taler-wallet-core/src/util/coinSelection.ts

193 lines
5.2 KiB
TypeScript
Raw Normal View History

/*
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.
*/
2021-11-27 20:56:58 +01:00
import {
AgeCommitmentProof,
2021-11-27 20:56:58 +01:00
AmountJson,
Amounts,
DenominationPubKey,
Logger,
2021-11-27 20:56:58 +01:00
} from "@gnu-taler/taler-util";
2021-03-27 19:35:44 +01:00
const logger = new Logger("coinSelection.ts");
/**
* 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.
*
* FIXME: We should only need the denomPubHash here, if at all.
*/
2021-11-17 10:23:22 +01:00
denomPub: DenominationPubKey;
/**
* Full value of the coin.
*/
value: AmountJson;
/**
* 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;
maxAge: number;
ageCommitmentProof?: AgeCommitmentProof;
}
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;
requiredMinimumAge?: number;
}
export interface CoinSelectionTally {
2021-03-27 19:35:44 +01:00
/**
* 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.
*/
export function tallyFees(
2021-03-27 19:35:44 +01:00
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 =
2022-11-02 17:42:14 +01:00
wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.zeroOfCurrency(currency);
2021-03-27 19:35:44 +01:00
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,
};
}