diff options
author | Florian Dold <florian@dold.me> | 2022-06-10 13:03:47 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-06-10 13:03:47 +0200 |
commit | f57dc7bf7a1e3a14c67512ba67d92fa350c95c0e (patch) | |
tree | c4f94cd64373e787d8b43645e9fdca469e713a98 /packages/taler-wallet-core/src/util | |
parent | 3ebb1d18154375471e329f2bad40534d858cbe1e (diff) | |
download | wallet-core-f57dc7bf7a1e3a14c67512ba67d92fa350c95c0e.tar.xz |
wallet-core: implement and test forced coin/denom selection
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.test.ts | 2 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/coinSelection.ts | 138 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/util/retries.ts | 2 |
3 files changed, 110 insertions, 32 deletions
diff --git a/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts index ca7b76eb5..55c007bbc 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.test.ts @@ -31,6 +31,7 @@ function a(x: string): AmountJson { function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { return { + value: a(current), availableAmount: a(current), coinPub: "foobar", denomPub: { @@ -45,6 +46,7 @@ function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo { function fakeAciWithAgeRestriction(current: string, feeDeposit: string): AvailableCoinInfo { return { + value: a(current), availableAmount: a(current), coinPub: "foobar", denomPub: { diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index 080a5049d..b3439067e 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -29,43 +29,15 @@ import { AmountJson, Amounts, DenominationPubKey, + ForcedCoinSel, Logger, + PayCoinSelection, } from "@gnu-taler/taler-util"; +import { checkLogicInvariant } from "./invariants.js"; 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. */ @@ -83,6 +55,11 @@ export interface AvailableCoinInfo { denomPub: DenominationPubKey; /** + * Full value of the coin. + */ + value: AmountJson; + + /** * Amount still remaining (typically the full amount, * as coins are always refreshed after use.) */ @@ -356,3 +333,102 @@ export function selectPayCoins( } return undefined; } + +export function selectForcedPayCoins( + forcedCoinSel: ForcedCoinSel, + 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(), + }; + + // Not supported by forced coin selection + checkLogicInvariant(!req.prevPayCoins); + + // 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) || + DenominationPubKey.cmp(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. + + // Set of spent coin indices from candidate coins + const spentSet: Set<number> = new Set(); + + for (const forcedCoin of forcedCoinSel.coins) { + let aci: AvailableCoinInfo | undefined = undefined; + for (let i = 0; i < candidateCoins.length; i++) { + if (spentSet.has(i)) { + continue; + } + if ( + Amounts.cmp(forcedCoin.value, candidateCoins[i].availableAmount) != 0 + ) { + continue; + } + spentSet.add(i); + aci = candidateCoins[i]; + break; + } + + if (!aci) { + throw Error("can't find coin for forced coin selection"); + } + + tally = tallyFees( + tally, + candidates.wireFeesPerExchange, + wireFeeAmortization, + aci.exchangeBaseUrl, + aci.feeDeposit, + ); + + let coinSpend = Amounts.parseOrThrow(forcedCoin.contribution); + + 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; +} diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts index 2fe18cb2c..13a05b385 100644 --- a/packages/taler-wallet-core/src/util/retries.ts +++ b/packages/taler-wallet-core/src/util/retries.ts @@ -37,7 +37,7 @@ export interface RetryPolicy { const defaultRetryPolicy: RetryPolicy = { backoffBase: 1.5, - backoffDelta: Duration.fromSpec({ seconds: 30 }), + backoffDelta: Duration.fromSpec({ seconds: 1 }), maxTimeout: Duration.fromSpec({ minutes: 2 }), }; |