summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-03-06 14:17:31 +0100
committerFlorian Dold <florian@dold.me>2024-03-06 20:34:42 +0100
commit91be5b89cd92c53d6aa2f68247f9626c8bc8f64a (patch)
tree2c562c9170d32833647d2bffb69a91c42bef9b0d /packages/taler-wallet-core
parent2e344093305ddf72f97e099cba107356970bb1e4 (diff)
downloadwallet-core-91be5b89cd92c53d6aa2f68247f9626c8bc8f64a.tar.gz
wallet-core-91be5b89cd92c53d6aa2f68247f9626c8bc8f64a.tar.bz2
wallet-core-91be5b89cd92c53d6aa2f68247f9626c8bc8f64a.zip
towards refactoring coin selection
Diffstat (limited to 'packages/taler-wallet-core')
-rw-r--r--packages/taler-wallet-core/src/balance.ts257
-rw-r--r--packages/taler-wallet-core/src/coinSelection.test.ts101
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts1202
-rw-r--r--packages/taler-wallet-core/src/deposits.ts22
-rw-r--r--packages/taler-wallet-core/src/pay-merchant.ts32
5 files changed, 811 insertions, 803 deletions
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
index 6dc0783c0..a77358363 100644
--- a/packages/taler-wallet-core/src/balance.ts
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -468,12 +468,16 @@ export interface MerchantPaymentBalanceDetails {
balanceAvailable: AmountJson;
}
-export interface MerchantPaymentRestrictionsForBalance {
+export interface PaymentRestrictionsForBalance {
currency: string;
minAge: number;
- acceptedExchanges: AllowedExchangeInfo[];
- acceptedAuditors: AllowedAuditorInfo[];
- acceptedWireMethods: string[];
+ restrictExchanges:
+ | {
+ exchanges: AllowedExchangeInfo[];
+ auditors: AllowedAuditorInfo[];
+ }
+ | undefined;
+ restrictWireMethods: string[] | undefined;
}
export interface AcceptableExchanges {
@@ -492,69 +496,73 @@ export interface AcceptableExchanges {
/**
* Get all exchanges that are acceptable for a particular payment.
*/
-export async function getAcceptableExchangeBaseUrls(
+async function getAcceptableExchangeBaseUrlsInTx(
wex: WalletExecutionContext,
- req: MerchantPaymentRestrictionsForBalance,
+ tx: WalletDbReadOnlyTransaction<["exchanges", "exchangeDetails"]>,
+ req: PaymentRestrictionsForBalance,
): Promise<AcceptableExchanges> {
const acceptableExchangeUrls = new Set<string>();
const depositableExchangeUrls = new Set<string>();
- await wex.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => {
- // FIXME: We should have a DB index to look up all exchanges
- // for a particular auditor ...
+ // FIXME: We should have a DB index to look up all exchanges
+ // for a particular auditor ...
- const canonExchanges = new Set<string>();
- const canonAuditors = new Set<string>();
+ const canonExchanges = new Set<string>();
+ const canonAuditors = new Set<string>();
- for (const exchangeHandle of req.acceptedExchanges) {
+ if (req.restrictExchanges) {
+ for (const exchangeHandle of req.restrictExchanges.exchanges) {
const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
canonExchanges.add(normUrl);
}
- for (const auditorHandle of req.acceptedAuditors) {
+ for (const auditorHandle of req.restrictExchanges.auditors) {
const normUrl = canonicalizeBaseUrl(auditorHandle.auditorBaseUrl);
canonAuditors.add(normUrl);
}
+ }
- await tx.exchanges.iter().forEachAsync(async (exchange) => {
- const dp = exchange.detailsPointer;
- if (!dp) {
- return;
- }
- const { currency, masterPublicKey } = dp;
- const exchangeDetails = await tx.exchangeDetails.indexes.byPointer.get([
- exchange.baseUrl,
- currency,
- masterPublicKey,
- ]);
- if (!exchangeDetails) {
- return;
- }
+ await tx.exchanges.iter().forEachAsync(async (exchange) => {
+ const dp = exchange.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency, masterPublicKey } = dp;
+ const exchangeDetails = await tx.exchangeDetails.indexes.byPointer.get([
+ exchange.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
+ if (!exchangeDetails) {
+ return;
+ }
- let acceptable = false;
+ let acceptable = false;
- if (canonExchanges.has(exchange.baseUrl)) {
+ if (canonExchanges.has(exchange.baseUrl)) {
+ acceptableExchangeUrls.add(exchange.baseUrl);
+ acceptable = true;
+ }
+ for (const exchangeAuditor of exchangeDetails.auditors) {
+ if (canonAuditors.has(exchangeAuditor.auditor_url)) {
acceptableExchangeUrls.add(exchange.baseUrl);
acceptable = true;
+ break;
}
- for (const exchangeAuditor of exchangeDetails.auditors) {
- if (canonAuditors.has(exchangeAuditor.auditor_url)) {
- acceptableExchangeUrls.add(exchange.baseUrl);
- acceptable = true;
- break;
- }
- }
+ }
- if (!acceptable) {
- return;
- }
- // FIXME: Also consider exchange and auditor public key
- // instead of just base URLs?
+ if (!acceptable) {
+ return;
+ }
+ // FIXME: Also consider exchange and auditor public key
+ // instead of just base URLs?
+
+ let wireMethodSupported = false;
- let wireMethodSupported = false;
+ if (req.restrictWireMethods) {
for (const acc of exchangeDetails.wireInfo.accounts) {
const pp = parsePaytoUri(acc.payto_uri);
checkLogicInvariant(!!pp);
- for (const wm of req.acceptedWireMethods) {
+ for (const wm of req.restrictWireMethods) {
if (pp.targetType === wm) {
wireMethodSupported = true;
break;
@@ -564,12 +572,14 @@ export async function getAcceptableExchangeBaseUrls(
}
}
}
+ } else {
+ wireMethodSupported = true;
+ }
- acceptableExchangeUrls.add(exchange.baseUrl);
- if (wireMethodSupported) {
- depositableExchangeUrls.add(exchange.baseUrl);
- }
- });
+ acceptableExchangeUrls.add(exchange.baseUrl);
+ if (wireMethodSupported) {
+ depositableExchangeUrls.add(exchange.baseUrl);
+ }
});
return {
acceptableExchanges: [...acceptableExchangeUrls],
@@ -606,9 +616,24 @@ export interface MerchantPaymentBalanceDetails {
export async function getMerchantPaymentBalanceDetails(
wex: WalletExecutionContext,
- req: MerchantPaymentRestrictionsForBalance,
+ req: PaymentRestrictionsForBalance,
): Promise<MerchantPaymentBalanceDetails> {
- const acceptability = await getAcceptableExchangeBaseUrls(wex, req);
+ return await wex.db.runReadOnlyTx(
+ ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"],
+ async (tx) => {
+ return getMerchantPaymentBalanceDetailsInTx(wex, tx, req);
+ },
+ );
+}
+
+export async function getMerchantPaymentBalanceDetailsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"]
+ >,
+ req: PaymentRestrictionsForBalance,
+): Promise<MerchantPaymentBalanceDetails> {
+ const acceptability = await getAcceptableExchangeBaseUrlsInTx(wex, tx, req);
const d: MerchantPaymentBalanceDetails = {
balanceAvailable: Amounts.zeroOfCurrency(req.currency),
@@ -618,53 +643,46 @@ export async function getMerchantPaymentBalanceDetails(
balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency),
};
- await wex.db.runReadOnlyTx(
- ["coinAvailability", "refreshGroups"],
- async (tx) => {
- await tx.coinAvailability.iter().forEach((ca) => {
- if (ca.currency != req.currency) {
- return;
- }
- const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
- const coinAmount: AmountJson = Amounts.mult(
- singleCoinAmount,
- ca.freshCoinCount,
+ await tx.coinAvailability.iter().forEach((ca) => {
+ if (ca.currency != req.currency) {
+ return;
+ }
+ const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+ const coinAmount: AmountJson = Amounts.mult(
+ singleCoinAmount,
+ ca.freshCoinCount,
+ ).amount;
+ d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
+ d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
+ if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
+ d.balanceAgeAcceptable = Amounts.add(
+ d.balanceAgeAcceptable,
+ coinAmount,
+ ).amount;
+ if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
+ d.balanceMerchantAcceptable = Amounts.add(
+ d.balanceMerchantAcceptable,
+ coinAmount,
).amount;
- d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
- d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
- if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
- d.balanceAgeAcceptable = Amounts.add(
- d.balanceAgeAcceptable,
+ if (acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)) {
+ d.balanceMerchantDepositable = Amounts.add(
+ d.balanceMerchantDepositable,
coinAmount,
).amount;
- if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
- d.balanceMerchantAcceptable = Amounts.add(
- d.balanceMerchantAcceptable,
- coinAmount,
- ).amount;
- if (
- acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)
- ) {
- d.balanceMerchantDepositable = Amounts.add(
- d.balanceMerchantDepositable,
- coinAmount,
- ).amount;
- }
- }
}
- });
+ }
+ }
+ });
- await tx.refreshGroups.iter().forEach((r) => {
- if (r.currency != req.currency) {
- return;
- }
- d.balanceAvailable = Amounts.add(
- d.balanceAvailable,
- computeRefreshGroupAvailableAmount(r),
- ).amount;
- });
- },
- );
+ await tx.refreshGroups.iter().forEach((r) => {
+ if (r.currency != req.currency) {
+ return;
+ }
+ d.balanceAvailable = Amounts.add(
+ d.balanceAvailable,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
+ });
return d;
}
@@ -697,9 +715,11 @@ export async function getBalanceDetail(
return await getMerchantPaymentBalanceDetails(wex, {
currency: req.currency,
- acceptedAuditors: [],
- acceptedExchanges: exchanges,
- acceptedWireMethods: wires,
+ restrictExchanges: {
+ auditors: [],
+ exchanges,
+ },
+ restrictWireMethods: wires,
minAge: 0,
});
}
@@ -763,3 +783,50 @@ export async function getPeerPaymentBalanceDetailsInTx(
balanceMaterial,
};
}
+
+/**
+ * Get information about the balance at a given exchange
+ * with certain restrictions.
+ */
+export async function getExchangePaymentBalanceDetailsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coinAvailability", "refreshGroups"]>,
+ req: PeerPaymentRestrictionsForBalance,
+): Promise<PeerPaymentBalanceDetails> {
+ let balanceAvailable = Amounts.zeroOfCurrency(req.currency);
+ let balanceMaterial = Amounts.zeroOfCurrency(req.currency);
+
+ await tx.coinAvailability.iter().forEach((ca) => {
+ if (ca.currency != req.currency) {
+ return;
+ }
+ if (
+ req.restrictExchangeTo &&
+ req.restrictExchangeTo !== ca.exchangeBaseUrl
+ ) {
+ return;
+ }
+ const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+ const coinAmount: AmountJson = Amounts.mult(
+ singleCoinAmount,
+ ca.freshCoinCount,
+ ).amount;
+ balanceAvailable = Amounts.add(balanceAvailable, coinAmount).amount;
+ balanceMaterial = Amounts.add(balanceMaterial, coinAmount).amount;
+ });
+
+ await tx.refreshGroups.iter().forEach((r) => {
+ if (r.currency != req.currency) {
+ return;
+ }
+ balanceAvailable = Amounts.add(
+ balanceAvailable,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
+ });
+
+ return {
+ balanceAvailable,
+ balanceMaterial,
+ };
+}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
index 4fac244fc..6eae9deaa 100644
--- a/packages/taler-wallet-core/src/coinSelection.test.ts
+++ b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -24,8 +24,8 @@ import {
import test from "ava";
import {
AvailableDenom,
- PeerCoinSelectionTally,
- testing_greedySelectPeer,
+ CoinSelectionTally,
+ emptyTallyForPeerPayment,
testing_selectGreedy,
} from "./coinSelection.js";
@@ -42,12 +42,13 @@ const inThePast = AbsoluteTime.toProtocolTimestamp(
test("p2p: should select the coin", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:2");
- const tally = {
- amountRemaining: instructedAmount,
- depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
- lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
- } satisfies PeerCoinSelectionTally;
- const coins = testing_greedySelectPeer(
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ t.log(`tally before: ${j2s(tally)}`);
+ const coins = testing_selectGreedy(
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
createCandidates([
{
amount: "LOCAL:10" as AmountString,
@@ -59,7 +60,8 @@ test("p2p: should select the coin", (t) => {
tally,
);
- t.log(j2s(coins));
+ t.log(`coins: ${j2s(coins)}`);
+ t.log(`tally: ${j2s(tally)}`);
t.assert(coins != null);
@@ -73,22 +75,16 @@ test("p2p: should select the coin", (t) => {
expireWithdraw: inTheDistantFuture,
},
});
-
- t.deepEqual(tally, {
- amountRemaining: Amounts.parseOrThrow("LOCAL:0"),
- depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.1"),
- lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
- });
});
test("p2p: should select 3 coins", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:20");
- const tally = {
- amountRemaining: instructedAmount,
- depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
- lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
- } satisfies PeerCoinSelectionTally;
- const coins = testing_greedySelectPeer(
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const coins = testing_selectGreedy(
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
createCandidates([
{
amount: "LOCAL:10" as AmountString,
@@ -114,22 +110,16 @@ test("p2p: should select 3 coins", (t) => {
expireWithdraw: inTheDistantFuture,
},
});
-
- t.deepEqual(tally, {
- amountRemaining: Amounts.parseOrThrow("LOCAL:0"),
- depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.3"),
- lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
- });
});
test("p2p: can't select since the instructed amount is too high", (t) => {
const instructedAmount = Amounts.parseOrThrow("LOCAL:60");
- const tally = {
- amountRemaining: instructedAmount,
- depositFeesAcc: Amounts.zeroOfCurrency(instructedAmount.currency),
- lastDepositFee: Amounts.zeroOfCurrency(instructedAmount.currency),
- } satisfies PeerCoinSelectionTally;
- const coins = testing_greedySelectPeer(
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const coins = testing_selectGreedy(
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
createCandidates([
{
amount: "LOCAL:10" as AmountString,
@@ -142,12 +132,6 @@ test("p2p: can't select since the instructed amount is too high", (t) => {
);
t.is(coins, undefined);
-
- t.deepEqual(tally, {
- amountRemaining: Amounts.parseOrThrow("LOCAL:10.5"),
- depositFeesAcc: Amounts.parseOrThrow("LOCAL:0.5"),
- lastDepositFee: Amounts.parseOrThrow("LOCAL:0.1"),
- });
});
test("pay: select one coin to pay with fee", (t) => {
@@ -162,22 +146,11 @@ test("pay: select one coin to pay with fee", (t) => {
customerWireFees: zero,
wireFeeCoveredForExchange: new Set<string>(),
lastDepositFee: zero,
- };
+ } satisfies CoinSelectionTally;
const coins = testing_selectGreedy(
{
- auditors: [],
- exchanges: [
- {
- exchangeBaseUrl: "http://exchange.localhost/",
- exchangePub: "E5M8CGRDHXF1RCVP3B8TQCTDYNQ7T4XHWR5SVEQRGVVMVME41VJ0",
- },
- ],
- contractTermsAmount: payment,
- depositFeeLimit: zero,
wireFeeAmortization: 1,
- wireFeeLimit: zero,
- prevPayCoins: [],
- wireMethod: "x-taler-bank",
+ wireFeesPerExchange: { "http://exchange.localhost/": exchangeWireFee },
},
createCandidates([
{
@@ -187,7 +160,6 @@ test("pay: select one coin to pay with fee", (t) => {
fromExchange: "http://exchange.localhost/",
},
]),
- { "http://exchange.localhost/": exchangeWireFee },
tally,
);
@@ -203,13 +175,13 @@ test("pay: select one coin to pay with fee", (t) => {
});
t.deepEqual(tally, {
- amountPayRemaining: Amounts.parseOrThrow("LOCAL:2"),
+ amountPayRemaining: Amounts.parseOrThrow("LOCAL:0"),
amountWireFeeLimitRemaining: zero,
amountDepositFeeLimitRemaining: zero,
- customerDepositFees: zero,
+ customerDepositFees: Amounts.parse("LOCAL:0.1"),
customerWireFees: zero,
- wireFeeCoveredForExchange: new Set(),
- lastDepositFee: zero,
+ wireFeeCoveredForExchange: new Set(["http://exchange.localhost/"]),
+ lastDepositFee: Amounts.parse("LOCAL:0.1"),
});
});
@@ -309,11 +281,14 @@ test("p2p: regression STATER", (t) => {
},
];
const instructedAmount = Amounts.parseOrThrow("STATER:1");
- const tally = {
- amountRemaining: instructedAmount,
- depositFeesAcc: Amounts.parseOrThrow("STATER:0"),
- lastDepositFee: Amounts.parseOrThrow("STATER:0"),
- } satisfies PeerCoinSelectionTally;
- const res = testing_greedySelectPeer(candidates as any, tally);
+ const tally = emptyTallyForPeerPayment(instructedAmount);
+ const res = testing_selectGreedy(
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
+ candidates as any,
+ tally,
+ );
t.assert(!!res);
});
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index 3ece5546c..c44ca3d17 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -39,7 +39,6 @@ import {
CoinPublicKeyString,
CoinStatus,
DenominationInfo,
- DenominationPubKey,
DenomSelectionState,
Duration,
ForcedCoinSel,
@@ -50,62 +49,25 @@ import {
parsePaytoUri,
PayCoinSelection,
PayMerchantInsufficientBalanceDetails,
- PayPeerInsufficientBalanceDetails,
strcmp,
TalerProtocolTimestamp,
UnblindedSignature,
} from "@gnu-taler/taler-util";
import {
- getMerchantPaymentBalanceDetails,
- getPeerPaymentBalanceDetailsInTx,
+ getExchangePaymentBalanceDetailsInTx,
+ getMerchantPaymentBalanceDetailsInTx,
} from "./balance.js";
import { getAutoRefreshExecuteThreshold } from "./common.js";
import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js";
import { isWithdrawableDenom } from "./denominations.js";
-import { getExchangeWireDetailsInTx } from "./exchanges.js";
+import {
+ ExchangeWireDetails,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
import { getDenomInfo, WalletExecutionContext } from "./wallet.js";
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.
- */
- 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;
@@ -113,19 +75,9 @@ export type PreviousPayCoins = {
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 ExchangeRestrictionSpec {
+ exchanges: AllowedExchangeInfo[];
+ auditors: AllowedAuditorInfo[];
}
export interface CoinSelectionTally {
@@ -159,26 +111,20 @@ export interface CoinSelectionTally {
* Account for the fees of spending a coin.
*/
function tallyFees(
- tally: Readonly<CoinSelectionTally>,
+ tally: CoinSelectionTally,
wireFeesPerExchange: Record<string, AmountJson>,
wireFeeAmortization: number,
exchangeBaseUrl: string,
feeDeposit: AmountJson,
-): CoinSelectionTally {
+): void {
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.zeroOfCurrency(currency);
- const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf);
- amountWireFeeLimitRemaining = Amounts.sub(
- amountWireFeeLimitRemaining,
+ const wfForgiven = Amounts.min(tally.amountWireFeeLimitRemaining, wf);
+ tally.amountWireFeeLimitRemaining = Amounts.sub(
+ tally.amountWireFeeLimitRemaining,
wfForgiven,
).amount;
// The remaining, amortized amount needs to be paid by the
@@ -187,45 +133,48 @@ function tallyFees(
Amounts.sub(wf, wfForgiven).amount,
wireFeeAmortization,
);
-
// This is the amount forgiven via the deposit fee allowance.
const wfDepositForgiven = Amounts.min(
- amountDepositFeeLimitRemaining,
+ tally.amountDepositFeeLimitRemaining,
wfRemaining,
);
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
+ tally.amountDepositFeeLimitRemaining = Amounts.sub(
+ tally.amountDepositFeeLimitRemaining,
wfDepositForgiven,
).amount;
-
wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
- customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
- wireFeeCoveredForExchange.add(exchangeBaseUrl);
+ tally.customerWireFees = Amounts.add(
+ tally.customerWireFees,
+ wfRemaining,
+ ).amount;
+ tally.amountPayRemaining = Amounts.add(
+ tally.amountPayRemaining,
+ wfRemaining,
+ ).amount;
+ tally.wireFeeCoveredForExchange.add(exchangeBaseUrl);
}
- const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
+ const dfForgiven = Amounts.min(
+ feeDeposit,
+ tally.amountDepositFeeLimitRemaining,
+ );
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
+ tally.amountDepositFeeLimitRemaining = Amounts.sub(
+ tally.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,
- lastDepositFee: feeDeposit,
- };
+ tally.customerDepositFees = Amounts.add(
+ tally.customerDepositFees,
+ dfRemaining,
+ ).amount;
+ tally.amountPayRemaining = Amounts.add(
+ tally.amountPayRemaining,
+ dfRemaining,
+ ).amount;
+ tally.lastDepositFee = feeDeposit;
}
export type SelectPayCoinsResult =
@@ -236,16 +185,13 @@ export type SelectPayCoinsResult =
| { type: "success"; coinSel: PayCoinSelection };
/**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
+ * 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 async function selectPayCoinsNew(
+export async function selectPayCoins(
wex: WalletExecutionContext,
req: SelectPayCoinRequestNg,
): Promise<SelectPayCoinsResult> {
@@ -256,141 +202,203 @@ export async function selectPayCoinsNew(
wireFeeAmortization,
} = req;
- // FIXME: Why don't we do this in a transaction?
- const [candidateDenoms, wireFeesPerExchange] =
- await selectPayMerchantCandidates(wex, req);
+ return await wex.db.runReadOnlyTx(
+ [
+ "coinAvailability",
+ "denominations",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "coins",
+ ],
+ async (tx) => {
+ const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ restrictWireMethod: req.restrictWireMethod,
+ depositPaytoUri: req.depositPaytoUri,
+ requiredMinimumAge: req.requiredMinimumAge,
+ },
+ );
- const coinPubs: string[] = [];
- const coinContributions: AmountJson[] = [];
- const currency = contractTermsAmount.currency;
+ const coinPubs: string[] = [];
+ const coinContributions: AmountJson[] = [];
+ const currency = contractTermsAmount.currency;
+
+ let tally: CoinSelectionTally = {
+ amountPayRemaining: contractTermsAmount,
+ amountWireFeeLimitRemaining: wireFeeLimit,
+ amountDepositFeeLimitRemaining: depositFeeLimit,
+ customerDepositFees: Amounts.zeroOfCurrency(currency),
+ customerWireFees: Amounts.zeroOfCurrency(currency),
+ wireFeeCoveredForExchange: new Set(),
+ lastDepositFee: Amounts.zeroOfCurrency(currency),
+ };
- let tally: CoinSelectionTally = {
- amountPayRemaining: contractTermsAmount,
- amountWireFeeLimitRemaining: wireFeeLimit,
- amountDepositFeeLimitRemaining: depositFeeLimit,
- customerDepositFees: Amounts.zeroOfCurrency(currency),
- customerWireFees: Amounts.zeroOfCurrency(currency),
- wireFeeCoveredForExchange: new Set(),
- lastDepositFee: Amounts.zeroOfCurrency(currency),
- };
+ const prevPayCoins = req.prevPayCoins ?? [];
- const prevPayCoins = req.prevPayCoins ?? [];
+ // Look at existing pay coin selection and tally up
+ for (const prev of prevPayCoins) {
+ tallyFees(
+ tally,
+ wireFeesPerExchange,
+ wireFeeAmortization,
+ prev.exchangeBaseUrl,
+ prev.feeDeposit,
+ );
+ tally.amountPayRemaining = Amounts.sub(
+ tally.amountPayRemaining,
+ prev.contribution,
+ ).amount;
- // Look at existing pay coin selection and tally up
- for (const prev of prevPayCoins) {
- tally = tallyFees(
- tally,
- wireFeesPerExchange,
- wireFeeAmortization,
- prev.exchangeBaseUrl,
- prev.feeDeposit,
- );
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- prev.contribution,
- ).amount;
+ coinPubs.push(prev.coinPub);
+ coinContributions.push(prev.contribution);
+ }
- coinPubs.push(prev.coinPub);
- coinContributions.push(prev.contribution);
- }
+ let selectedDenom: SelResult | undefined;
+ if (req.forcedSelection) {
+ selectedDenom = selectForced(req, candidateDenoms);
+ } else {
+ // 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.
+ selectedDenom = selectGreedy(
+ {
+ wireFeeAmortization: req.wireFeeAmortization,
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ candidateDenoms,
+ tally,
+ );
+ }
- let selectedDenom: SelResult | undefined;
- if (req.forcedSelection) {
- selectedDenom = selectForced(req, candidateDenoms);
- } else {
- // 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.
- selectedDenom = selectGreedy(
- req,
- candidateDenoms,
- wireFeesPerExchange,
- tally,
- );
- }
+ if (!selectedDenom) {
+ return {
+ type: "failure",
+ insufficientBalanceDetails: await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: req.restrictExchanges,
+ instructedAmount: req.contractTermsAmount,
+ requiredMinimumAge: req.requiredMinimumAge,
+ wireMethod: req.restrictWireMethod,
+ },
+ ),
+ } satisfies SelectPayCoinsResult;
+ }
- if (!selectedDenom) {
- const details = await getMerchantPaymentBalanceDetails(wex, {
- acceptedAuditors: req.auditors,
- acceptedExchanges: req.exchanges,
- acceptedWireMethods: [req.wireMethod],
- currency: Amounts.currencyOf(req.contractTermsAmount),
- minAge: req.requiredMinimumAge ?? 0,
- });
- let feeGapEstimate: AmountJson;
- if (
- Amounts.cmp(
- details.balanceMerchantDepositable,
- req.contractTermsAmount,
- ) >= 0
- ) {
- // FIXME: We can probably give a better estimate.
- feeGapEstimate = Amounts.add(
- tally.amountPayRemaining,
- tally.lastDepositFee,
- ).amount;
- } else {
- feeGapEstimate = Amounts.zeroOfAmount(req.contractTermsAmount);
+ const finalSel = selectedDenom;
+
+ logger.trace(`coin selection request ${j2s(req)}`);
+ logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
+
+ for (const dph of Object.keys(finalSel)) {
+ const selInfo = finalSel[dph];
+ const numRequested = selInfo.contributions.length;
+ const query = [
+ selInfo.exchangeBaseUrl,
+ selInfo.denomPubHash,
+ selInfo.maxAge,
+ CoinStatus.Fresh,
+ ];
+ logger.trace(`query: ${j2s(query)}`);
+ const coins =
+ await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
+ query,
+ numRequested,
+ );
+ if (coins.length != numRequested) {
+ throw Error(
+ `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
+ );
+ }
+ coinPubs.push(...coins.map((x) => x.coinPub));
+ coinContributions.push(...selInfo.contributions);
+ }
+
+ return {
+ type: "success",
+ coinSel: {
+ paymentAmount: Amounts.stringify(contractTermsAmount),
+ coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
+ coinPubs,
+ customerDepositFees: Amounts.stringify(tally.customerDepositFees),
+ customerWireFees: Amounts.stringify(tally.customerWireFees),
+ },
+ };
+ },
+ );
+}
+
+interface ReportInsufficientBalanceRequest {
+ instructedAmount: AmountJson;
+ requiredMinimumAge: number | undefined;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ wireMethod: string | undefined;
+}
+
+export async function reportInsufficientBalanceDetails(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ ["coinAvailability", "exchanges", "exchangeDetails", "refreshGroups"]
+ >,
+ req: ReportInsufficientBalanceRequest,
+): Promise<PayMerchantInsufficientBalanceDetails> {
+ const currency = Amounts.currencyOf(req.instructedAmount);
+ const details = await getMerchantPaymentBalanceDetailsInTx(wex, tx, {
+ restrictExchanges: req.restrictExchanges,
+ restrictWireMethods: req.wireMethod ? [req.wireMethod] : [],
+ currency: Amounts.currencyOf(req.instructedAmount),
+ minAge: req.requiredMinimumAge ?? 0,
+ });
+ let feeGapEstimate: AmountJson;
+
+ // FIXME: need fee gap estimate
+ // FIXME: We can probably give a better estimate.
+ // feeGapEstimate = Amounts.add(
+ // tally.amountPayRemaining,
+ // tally.lastDepositFee,
+ // ).amount;
+
+ feeGapEstimate = Amounts.zeroOfAmount(req.instructedAmount);
+
+ const perExchange: PayMerchantInsufficientBalanceDetails["perExchange"] = {};
+
+ const exchanges = await tx.exchanges.iter().toArray();
+
+ for (const exch of exchanges) {
+ if (exch.detailsPointer?.currency !== currency) {
+ continue;
}
- return {
- type: "failure",
- insufficientBalanceDetails: {
- amountRequested: Amounts.stringify(req.contractTermsAmount),
- balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
- balanceAvailable: Amounts.stringify(details.balanceAvailable),
- balanceMaterial: Amounts.stringify(details.balanceMaterial),
- balanceMerchantAcceptable: Amounts.stringify(
- details.balanceMerchantAcceptable,
- ),
- balanceMerchantDepositable: Amounts.stringify(
- details.balanceMerchantDepositable,
- ),
- feeGapEstimate: Amounts.stringify(feeGapEstimate),
- },
+ const infoExchange = await getExchangePaymentBalanceDetailsInTx(wex, tx, {
+ currency,
+ restrictExchangeTo: exch.baseUrl,
+ });
+ perExchange[exch.baseUrl] = {
+ balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
+ balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
+ feeGapEstimate: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
};
}
- const finalSel = selectedDenom;
-
- logger.trace(`coin selection request ${j2s(req)}`);
- logger.trace(`selected coins (via denoms) for payment: ${j2s(finalSel)}`);
-
- await wex.db.runReadOnlyTx(["coins", "denominations"], async (tx) => {
- for (const dph of Object.keys(finalSel)) {
- const selInfo = finalSel[dph];
- const numRequested = selInfo.contributions.length;
- const query = [
- selInfo.exchangeBaseUrl,
- selInfo.denomPubHash,
- selInfo.maxAge,
- CoinStatus.Fresh,
- ];
- logger.trace(`query: ${j2s(query)}`);
- const coins =
- await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
- query,
- numRequested,
- );
- if (coins.length != numRequested) {
- throw Error(
- `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
- );
- }
- coinPubs.push(...coins.map((x) => x.coinPub));
- coinContributions.push(...selInfo.contributions);
- }
- });
-
return {
- type: "success",
- coinSel: {
- paymentAmount: Amounts.stringify(contractTermsAmount),
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- coinPubs,
- customerDepositFees: Amounts.stringify(tally.customerDepositFees),
- customerWireFees: Amounts.stringify(tally.customerWireFees),
- },
+ amountRequested: Amounts.stringify(req.instructedAmount),
+ balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
+ balanceAvailable: Amounts.stringify(details.balanceAvailable),
+ balanceMaterial: Amounts.stringify(details.balanceMaterial),
+ balanceMerchantAcceptable: Amounts.stringify(
+ details.balanceMerchantAcceptable,
+ ),
+ balanceMerchantDepositable: Amounts.stringify(
+ details.balanceMerchantDepositable,
+ ),
+ feeGapEstimate: Amounts.stringify(feeGapEstimate),
+ perExchange,
};
}
@@ -426,10 +434,14 @@ export function testing_selectGreedy(
return selectGreedy(...args);
}
+export interface SelectGreedyRequest {
+ wireFeeAmortization: number;
+ wireFeesPerExchange: Record<string, AmountJson>;
+}
+
function selectGreedy(
- req: SelectPayCoinRequestNg,
+ req: SelectGreedyRequest,
candidateDenoms: AvailableDenom[],
- wireFeesPerExchange: Record<string, AmountJson>,
tally: CoinSelectionTally,
): SelResult | undefined {
const { wireFeeAmortization } = req;
@@ -449,9 +461,9 @@ function selectGreedy(
i < denom.numAvailable && Amounts.isNonZero(tally.amountPayRemaining);
i++
) {
- tally = tallyFees(
+ tallyFees(
tally,
- wireFeesPerExchange,
+ req.wireFeesPerExchange,
wireFeeAmortization,
denom.exchangeBaseUrl,
Amounts.parseOrThrow(denom.feeDeposit),
@@ -491,6 +503,7 @@ function selectGreedy(
selectedDenom[avKey] = sd;
}
}
+ logger.info(`greedy tally: ${j2s(tally)}`);
return Amounts.isZero(tally.amountPayRemaining) ? selectedDenom : undefined;
}
@@ -566,9 +579,8 @@ export function checkAccountRestriction(
}
export interface SelectPayCoinRequestNg {
- exchanges: AllowedExchangeInfo[];
- auditors: AllowedAuditorInfo[];
- wireMethod: string;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ restrictWireMethod: string;
contractTermsAmount: AmountJson;
depositFeeLimit: AmountJson;
wireFeeLimit: AmountJson;
@@ -592,137 +604,183 @@ export type AvailableDenom = DenominationInfo & {
numAvailable: number;
};
-async function selectPayMerchantCandidates(
+function findMatchingWire(
+ wireMethod: string,
+ depositPaytoUri: string | undefined,
+ exchangeWireDetails: ExchangeWireDetails,
+): { wireFee: AmountJson } | undefined {
+ for (const acc of exchangeWireDetails.wireInfo.accounts) {
+ const pp = parsePaytoUri(acc.payto_uri);
+ checkLogicInvariant(!!pp);
+ if (pp.targetType !== wireMethod) {
+ continue;
+ }
+ const wireFeeStr = exchangeWireDetails.wireInfo.feesForType[
+ wireMethod
+ ]?.find((x) => {
+ return AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(x.startStamp),
+ AbsoluteTime.fromProtocolTimestamp(x.endStamp),
+ );
+ })?.wireFee;
+
+ if (!wireFeeStr) {
+ continue;
+ }
+
+ let debitAccountCheckOk = false;
+ if (depositPaytoUri) {
+ // FIXME: We should somehow propagate the hint here!
+ const checkResult = checkAccountRestriction(
+ depositPaytoUri,
+ acc.debit_restrictions,
+ );
+ if (checkResult.ok) {
+ debitAccountCheckOk = true;
+ }
+ } else {
+ debitAccountCheckOk = true;
+ }
+
+ if (!debitAccountCheckOk) {
+ continue;
+ }
+
+ return {
+ wireFee: Amounts.parseOrThrow(wireFeeStr),
+ };
+ }
+ return undefined;
+}
+
+function checkExchangeAccepted(
+ exchangeDetails: ExchangeWireDetails,
+ exchangeRestrictions: ExchangeRestrictionSpec | undefined,
+): boolean {
+ if (!exchangeRestrictions) {
+ return true;
+ }
+ let accepted = false;
+ for (const allowedExchange of exchangeRestrictions.exchanges) {
+ if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+ accepted = true;
+ break;
+ }
+ }
+ for (const allowedAuditor of exchangeRestrictions.auditors) {
+ for (const providedAuditor of exchangeDetails.auditors) {
+ if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
+ accepted = true;
+ break;
+ }
+ }
+ }
+ return accepted;
+}
+
+interface SelectPayCandidatesRequest {
+ instructedAmount: AmountJson;
+ restrictWireMethod: string | undefined;
+ depositPaytoUri?: string;
+ restrictExchanges:
+ | {
+ exchanges: AllowedExchangeInfo[];
+ auditors: AllowedAuditorInfo[];
+ }
+ | undefined;
+ requiredMinimumAge?: number;
+}
+
+async function selectPayCandidates(
wex: WalletExecutionContext,
- req: SelectPayCoinRequestNg,
+ tx: WalletDbReadOnlyTransaction<
+ ["exchanges", "coinAvailability", "exchangeDetails", "denominations"]
+ >,
+ req: SelectPayCandidatesRequest,
): Promise<[AvailableDenom[], Record<string, AmountJson>]> {
- return await wex.db.runReadOnlyTx(
- ["exchanges", "exchangeDetails", "denominations", "coinAvailability"],
- async (tx) => {
- // FIXME: Use the existing helper (from balance.ts) to
- // get acceptable exchanges.
- const denoms: AvailableDenom[] = [];
- const exchanges = await tx.exchanges.iter().toArray();
- const wfPerExchange: Record<string, AmountJson> = {};
- loopExchange: for (const exchange of exchanges) {
- const exchangeDetails = await getExchangeWireDetailsInTx(
- tx,
- exchange.baseUrl,
- );
- // 1.- exchange has same currency
- if (exchangeDetails?.currency !== req.contractTermsAmount.currency) {
- continue;
- }
- let wireMethodFee: string | undefined;
- // 2.- exchange supports wire method
- loopWireAccount: for (const acc of exchangeDetails.wireInfo.accounts) {
- const pp = parsePaytoUri(acc.payto_uri);
- checkLogicInvariant(!!pp);
- if (pp.targetType !== req.wireMethod) {
- continue;
- }
- const wireFeeStr = exchangeDetails.wireInfo.feesForType[
- req.wireMethod
- ]?.find((x) => {
- return AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(x.startStamp),
- AbsoluteTime.fromProtocolTimestamp(x.endStamp),
- );
- })?.wireFee;
- let debitAccountCheckOk = false;
- if (req.depositPaytoUri) {
- // FIXME: We should somehow propagate the hint here!
- const checkResult = checkAccountRestriction(
- req.depositPaytoUri,
- acc.debit_restrictions,
- );
- if (checkResult.ok) {
- debitAccountCheckOk = true;
- }
- } else {
- debitAccountCheckOk = true;
- }
-
- if (wireFeeStr) {
- wireMethodFee = wireFeeStr;
- break loopWireAccount;
- }
- }
- if (!wireMethodFee) {
- continue;
- }
- wfPerExchange[exchange.baseUrl] = Amounts.parseOrThrow(wireMethodFee);
-
- // 3.- exchange is trusted in the exchange list or auditor list
- let accepted = false;
- for (const allowedExchange of req.exchanges) {
- if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
- accepted = true;
- break;
- }
- }
- for (const allowedAuditor of req.auditors) {
- for (const providedAuditor of exchangeDetails.auditors) {
- if (allowedAuditor.auditorPub === providedAuditor.auditor_pub) {
- accepted = true;
- break;
- }
- }
- }
- if (!accepted) {
- continue;
- }
- // 4.- filter coins restricted by age
- let ageLower = 0;
- let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
- if (req.requiredMinimumAge) {
- ageLower = req.requiredMinimumAge;
- }
- const myExchangeCoins =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeDetails.exchangeBaseUrl, ageLower, 1],
- [
- exchangeDetails.exchangeBaseUrl,
- ageUpper,
- Number.MAX_SAFE_INTEGER,
- ],
- ),
- );
- // 5.- save denoms with how many coins are available
- // FIXME: Check that the individual denomination is audited!
- // FIXME: Should we exclude denominations that are
- // not spendable anymore?
- for (const coinAvail of myExchangeCoins) {
- const denom = await tx.denominations.get([
- coinAvail.exchangeBaseUrl,
- coinAvail.denomPubHash,
- ]);
- checkDbInvariant(!!denom);
- if (denom.isRevoked || !denom.isOffered) {
- continue;
- }
- denoms.push({
- ...DenominationRecord.toDenomInfo(denom),
- numAvailable: coinAvail.freshCoinCount ?? 0,
- maxAge: coinAvail.maxAge,
- });
- }
+ // FIXME: Use the existing helper (from balance.ts) to
+ // get acceptable exchanges.
+ const denoms: AvailableDenom[] = [];
+ const exchanges = await tx.exchanges.iter().toArray();
+ const wfPerExchange: Record<string, AmountJson> = {};
+ for (const exchange of exchanges) {
+ const exchangeDetails = await getExchangeWireDetailsInTx(
+ tx,
+ exchange.baseUrl,
+ );
+ // 1. exchange has same currency
+ if (exchangeDetails?.currency !== req.instructedAmount.currency) {
+ continue;
+ }
+
+ // 2. Exchange supports wire method (only for pay/deposit)
+ if (req.restrictWireMethod) {
+ const wire = findMatchingWire(
+ req.restrictWireMethod,
+ req.depositPaytoUri,
+ exchangeDetails,
+ );
+ if (!wire) {
+ continue;
}
- logger.info(`available denoms ${j2s(denoms)}`);
- // Sort by available amount (descending), deposit fee (ascending) and
- // denomPub (ascending) if deposit fee is the same
- // (to guarantee deterministic results)
- denoms.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
+ }
+
+ // 3. exchange is trusted in the exchange list or auditor list
+ let accepted = checkExchangeAccepted(
+ exchangeDetails,
+ req.restrictExchanges,
+ );
+ if (!accepted) {
+ continue;
+ }
+
+ // 4. filter coins restricted by age
+ let ageLower = 0;
+ let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
+ if (req.requiredMinimumAge) {
+ ageLower = req.requiredMinimumAge;
+ }
+
+ const myExchangeCoins =
+ await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
+ GlobalIDB.KeyRange.bound(
+ [exchangeDetails.exchangeBaseUrl, ageLower, 1],
+ [exchangeDetails.exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+ ),
);
- return [denoms, wfPerExchange];
- },
+
+ // 5. save denoms with how many coins are available
+ // FIXME: Check that the individual denomination is audited!
+ // FIXME: Should we exclude denominations that are
+ // not spendable anymore?
+ for (const coinAvail of myExchangeCoins) {
+ const denom = await tx.denominations.get([
+ coinAvail.exchangeBaseUrl,
+ coinAvail.denomPubHash,
+ ]);
+ checkDbInvariant(!!denom);
+ if (denom.isRevoked || !denom.isOffered) {
+ continue;
+ }
+ denoms.push({
+ ...DenominationRecord.toDenomInfo(denom),
+ numAvailable: coinAvail.freshCoinCount ?? 0,
+ maxAge: coinAvail.maxAge,
+ });
+ }
+ }
+ logger.info(`available denoms ${j2s(denoms)}`);
+ // Sort by available amount (descending), deposit fee (ascending) and
+ // denomPub (ascending) if deposit fee is the same
+ // (to guarantee deterministic results)
+ denoms.sort(
+ (o1, o2) =>
+ -Amounts.cmp(o1.value, o2.value) ||
+ Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+ strcmp(o1.denomPubHash, o2.denomPubHash),
);
+ return [denoms, wfPerExchange];
}
/**
@@ -882,7 +940,7 @@ export type SelectPeerCoinsResult =
| { type: "success"; result: PeerCoinSelectionDetails }
| {
type: "failure";
- insufficientBalanceDetails: PayPeerInsufficientBalanceDetails;
+ insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
};
export interface PeerCoinRepair {
@@ -901,134 +959,134 @@ export interface PeerCoinSelectionRequest {
repair?: PeerCoinRepair;
}
-/**
- * Get coin availability information for a certain exchange.
- */
-async function selectPayPeerCandidatesForExchange(
- wex: WalletExecutionContext,
- tx: WalletDbReadOnlyTransaction<["coinAvailability", "denominations"]>,
+async function assemblePeerCoinSelectionDetails(
+ tx: WalletDbReadOnlyTransaction<["coins"]>,
exchangeBaseUrl: string,
-): Promise<AvailableDenom[]> {
- const denoms: AvailableDenom[] = [];
-
- let ageLower = 0;
- let ageUpper = AgeRestriction.AGE_UNRESTRICTED;
- const myExchangeCoins =
- await tx.coinAvailability.indexes.byExchangeAgeAvailability.getAll(
- GlobalIDB.KeyRange.bound(
- [exchangeBaseUrl, ageLower, 1],
- [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+ selectedDenom: SelResult,
+ resCoins: ResCoin[],
+ tally: CoinSelectionTally,
+): Promise<PeerCoinSelectionDetails> {
+ let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
+ for (const dph of Object.keys(selectedDenom)) {
+ const selInfo = selectedDenom[dph];
+ // Compute earliest time that a selected denom
+ // would have its coins auto-refreshed.
+ minAutorefreshExecuteThreshold = TalerProtocolTimestamp.min(
+ minAutorefreshExecuteThreshold,
+ AbsoluteTime.toProtocolTimestamp(
+ getAutoRefreshExecuteThreshold({
+ stampExpireDeposit: selInfo.expireDeposit,
+ stampExpireWithdraw: selInfo.expireWithdraw,
+ }),
),
);
-
- for (const coinAvail of myExchangeCoins) {
- if (coinAvail.freshCoinCount <= 0) {
- continue;
+ const numRequested = selInfo.contributions.length;
+ const query = [
+ selInfo.exchangeBaseUrl,
+ selInfo.denomPubHash,
+ selInfo.maxAge,
+ CoinStatus.Fresh,
+ ];
+ logger.info(`query: ${j2s(query)}`);
+ const coins =
+ await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
+ query,
+ numRequested,
+ );
+ if (coins.length != numRequested) {
+ throw Error(
+ `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
+ );
}
- const denom = await tx.denominations.get([
- coinAvail.exchangeBaseUrl,
- coinAvail.denomPubHash,
- ]);
- checkDbInvariant(!!denom);
- if (denom.isRevoked || !denom.isOffered) {
- continue;
+ for (let i = 0; i < selInfo.contributions.length; i++) {
+ resCoins.push({
+ coinPriv: coins[i].coinPriv,
+ coinPub: coins[i].coinPub,
+ contribution: Amounts.stringify(selInfo.contributions[i]),
+ ageCommitmentProof: coins[i].ageCommitmentProof,
+ denomPubHash: selInfo.denomPubHash,
+ denomSig: coins[i].denomSig,
+ });
}
- denoms.push({
- ...DenominationRecord.toDenomInfo(denom),
- numAvailable: coinAvail.freshCoinCount ?? 0,
- maxAge: coinAvail.maxAge,
- });
}
- // Sort by available amount (descending), deposit fee (ascending) and
- // denomPub (ascending) if deposit fee is the same
- // (to guarantee deterministic results)
- denoms.sort(
- (o1, o2) =>
- -Amounts.cmp(o1.value, o2.value) ||
- Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPubHash, o2.denomPubHash),
- );
-
- return denoms;
-}
-export interface PeerCoinSelectionTally {
- amountRemaining: AmountJson;
- depositFeesAcc: AmountJson;
- lastDepositFee: AmountJson;
-}
-
-/**
- * exporting for testing
- */
-export function testing_greedySelectPeer(
- ...args: Parameters<typeof greedySelectPeer>
-): ReturnType<typeof greedySelectPeer> {
- return greedySelectPeer(...args);
+ return {
+ exchangeBaseUrl,
+ coins: resCoins,
+ depositFees: tally.customerDepositFees,
+ maxExpirationDate: minAutorefreshExecuteThreshold,
+ };
}
-function greedySelectPeer(
- candidates: AvailableDenom[],
- tally: PeerCoinSelectionTally,
-): SelResult | undefined {
- const selectedDenom: SelResult = {};
- for (const denom of candidates) {
- const contributions: AmountJson[] = [];
- const feeDeposit = Amounts.parseOrThrow(denom.feeDeposit);
- for (
- let i = 0;
- i < denom.numAvailable && Amounts.isNonZero(tally.amountRemaining);
- i++
- ) {
- tally.depositFeesAcc = Amounts.add(
- tally.depositFeesAcc,
- feeDeposit,
- ).amount;
- tally.amountRemaining = Amounts.add(
- tally.amountRemaining,
- feeDeposit,
- ).amount;
- tally.lastDepositFee = feeDeposit;
-
- const coinSpend = Amounts.max(
- Amounts.min(tally.amountRemaining, denom.value),
- denom.feeDeposit,
+async function maybeRepairPeerCoinSelection(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ exchangeBaseUrl: string,
+ tally: CoinSelectionTally,
+ repair: PeerCoinRepair | undefined,
+): Promise<ResCoin[]> {
+ const resCoins: ResCoin[] = [];
+
+ if (repair && repair.exchangeBaseUrl === exchangeBaseUrl) {
+ for (let i = 0; i < repair.coinPubs.length; i++) {
+ const contrib = repair.contribs[i];
+ const coin = await tx.coins.get(repair.coinPubs[i]);
+ if (!coin) {
+ throw Error("repair not possible, coin not found");
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
);
-
- tally.amountRemaining = Amounts.sub(
- tally.amountRemaining,
- coinSpend,
+ checkDbInvariant(!!denom);
+ resCoins.push({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contribution: Amounts.stringify(contrib),
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ ageCommitmentProof: coin.ageCommitmentProof,
+ });
+ const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
+ tally.lastDepositFee = depositFee;
+ tally.amountPayRemaining = Amounts.sub(
+ tally.amountPayRemaining,
+ Amounts.sub(contrib, depositFee).amount,
+ ).amount;
+ tally.customerDepositFees = Amounts.add(
+ tally.customerDepositFees,
+ depositFee,
).amount;
-
- contributions.push(coinSpend);
- }
- if (contributions.length > 0) {
- const avKey = makeAvailabilityKey(
- denom.exchangeBaseUrl,
- denom.denomPubHash,
- denom.maxAge,
- );
- let sd = selectedDenom[avKey];
- if (!sd) {
- sd = {
- contributions: [],
- denomPubHash: denom.denomPubHash,
- exchangeBaseUrl: denom.exchangeBaseUrl,
- maxAge: denom.maxAge,
- expireDeposit: denom.stampExpireDeposit,
- expireWithdraw: denom.stampExpireWithdraw,
- };
- }
- sd.contributions.push(...contributions);
- selectedDenom[avKey] = sd;
}
}
+ return resCoins;
+}
- if (Amounts.isZero(tally.amountRemaining)) {
- return selectedDenom;
- }
+interface ResCoin {
+ coinPub: string;
+ coinPriv: string;
+ contribution: AmountString;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+}
- return undefined;
+export function emptyTallyForPeerPayment(
+ instructedAmount: AmountJson,
+): CoinSelectionTally {
+ const currency = instructedAmount.currency;
+ const zero = Amounts.zeroOfCurrency(currency);
+ return {
+ amountPayRemaining: instructedAmount,
+ customerDepositFees: zero,
+ lastDepositFee: zero,
+ amountDepositFeeLimitRemaining: zero,
+ amountWireFeeLimitRemaining: zero,
+ customerWireFees: zero,
+ wireFeeCoveredForExchange: new Set(),
+ };
}
export async function selectPeerCoins(
@@ -1041,6 +1099,7 @@ export async function selectPeerCoins(
// one coin to spend.
throw new Error("amount of zero not allowed");
}
+
return await wex.db.runReadWriteTx(
[
"exchanges",
@@ -1049,72 +1108,40 @@ export async function selectPeerCoins(
"coinAvailability",
"denominations",
"refreshGroups",
- "peerPushDebit",
+ "exchangeDetails",
],
- async (tx) => {
+ async (tx): Promise<SelectPeerCoinsResult> => {
const exchanges = await tx.exchanges.iter().toArray();
- const exchangeFeeGap: { [url: string]: AmountJson } = {};
const currency = Amounts.currencyOf(instructedAmount);
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== currency) {
continue;
}
- const candidates = await selectPayPeerCandidatesForExchange(
+ const candidatesRes = await selectPayCandidates(wex, tx, {
+ instructedAmount,
+ restrictExchanges: {
+ auditors: [],
+ exchanges: [
+ {
+ exchangeBaseUrl: exch.baseUrl,
+ exchangePub: exch.detailsPointer.masterPublicKey,
+ },
+ ],
+ },
+ restrictWireMethod: undefined,
+ });
+ const candidates = candidatesRes[0];
+ if (logger.shouldLogTrace()) {
+ logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
+ }
+ const tally = emptyTallyForPeerPayment(req.instructedAmount);
+ const resCoins: ResCoin[] = await maybeRepairPeerCoinSelection(
wex,
tx,
exch.baseUrl,
+ tally,
+ req.repair,
);
- if (logger.shouldLogTrace()) {
- logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
- }
- const tally: PeerCoinSelectionTally = {
- amountRemaining: Amounts.parseOrThrow(instructedAmount),
- depositFeesAcc: Amounts.zeroOfCurrency(currency),
- lastDepositFee: Amounts.zeroOfCurrency(currency),
- };
- const resCoins: {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }[] = [];
-
- if (req.repair && req.repair.exchangeBaseUrl === exch.baseUrl) {
- for (let i = 0; i < req.repair.coinPubs.length; i++) {
- const contrib = req.repair.contribs[i];
- const coin = await tx.coins.get(req.repair.coinPubs[i]);
- if (!coin) {
- throw Error("repair not possible, coin not found");
- }
- const denom = await getDenomInfo(
- wex,
- tx,
- coin.exchangeBaseUrl,
- coin.denomPubHash,
- );
- checkDbInvariant(!!denom);
- resCoins.push({
- coinPriv: coin.coinPriv,
- coinPub: coin.coinPub,
- contribution: Amounts.stringify(contrib),
- denomPubHash: coin.denomPubHash,
- denomSig: coin.denomSig,
- ageCommitmentProof: coin.ageCommitmentProof,
- });
- const depositFee = Amounts.parseOrThrow(denom.feeDeposit);
- tally.lastDepositFee = depositFee;
- tally.amountRemaining = Amounts.sub(
- tally.amountRemaining,
- Amounts.sub(contrib, depositFee).amount,
- ).amount;
- tally.depositFeesAcc = Amounts.add(
- tally.depositFeesAcc,
- depositFee,
- ).amount;
- }
- }
if (logger.shouldLogTrace()) {
logger.trace(`candidates: ${j2s(candidates)}`);
@@ -1122,113 +1149,42 @@ export async function selectPeerCoins(
logger.trace(`tally: ${j2s(tally)}`);
}
- const selectedDenom = greedySelectPeer(candidates, tally);
+ const selectedDenom = selectGreedy(
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
+ candidates,
+ tally,
+ );
if (selectedDenom) {
- let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
- for (const dph of Object.keys(selectedDenom)) {
- const selInfo = selectedDenom[dph];
- // Compute earliest time that a selected denom
- // would have its coins auto-refreshed.
- minAutorefreshExecuteThreshold = TalerProtocolTimestamp.min(
- minAutorefreshExecuteThreshold,
- AbsoluteTime.toProtocolTimestamp(
- getAutoRefreshExecuteThreshold({
- stampExpireDeposit: selInfo.expireDeposit,
- stampExpireWithdraw: selInfo.expireWithdraw,
- }),
- ),
- );
- const numRequested = selInfo.contributions.length;
- const query = [
- selInfo.exchangeBaseUrl,
- selInfo.denomPubHash,
- selInfo.maxAge,
- CoinStatus.Fresh,
- ];
- logger.info(`query: ${j2s(query)}`);
- const coins =
- await tx.coins.indexes.byExchangeDenomPubHashAndAgeAndStatus.getAll(
- query,
- numRequested,
- );
- if (coins.length != numRequested) {
- throw Error(
- `coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
- );
- }
- for (let i = 0; i < selInfo.contributions.length; i++) {
- resCoins.push({
- coinPriv: coins[i].coinPriv,
- coinPub: coins[i].coinPub,
- contribution: Amounts.stringify(selInfo.contributions[i]),
- ageCommitmentProof: coins[i].ageCommitmentProof,
- denomPubHash: selInfo.denomPubHash,
- denomSig: coins[i].denomSig,
- });
- }
- }
-
- const res: PeerCoinSelectionDetails = {
- exchangeBaseUrl: exch.baseUrl,
- coins: resCoins,
- depositFees: tally.depositFeesAcc,
- maxExpirationDate: minAutorefreshExecuteThreshold,
+ return {
+ type: "success",
+ result: await assemblePeerCoinSelectionDetails(
+ tx,
+ exch.baseUrl,
+ selectedDenom,
+ resCoins,
+ tally,
+ ),
};
- return { type: "success", result: res };
- }
-
- exchangeFeeGap[exch.baseUrl] = Amounts.add(
- tally.lastDepositFee,
- tally.amountRemaining,
- ).amount;
-
- continue;
- }
-
- // We were unable to select coins.
- // Now we need to produce error details.
-
- const infoGeneral = await getPeerPaymentBalanceDetailsInTx(wex, tx, {
- currency,
- });
-
- const perExchange: PayPeerInsufficientBalanceDetails["perExchange"] = {};
-
- let maxFeeGapEstimate = Amounts.zeroOfCurrency(currency);
-
- for (const exch of exchanges) {
- if (exch.detailsPointer?.currency !== currency) {
- continue;
- }
- const infoExchange = await getPeerPaymentBalanceDetailsInTx(wex, tx, {
- currency,
- restrictExchangeTo: exch.baseUrl,
- });
- let gap =
- exchangeFeeGap[exch.baseUrl] ?? Amounts.zeroOfCurrency(currency);
- if (Amounts.cmp(infoExchange.balanceMaterial, instructedAmount) < 0) {
- // Show fee gap only if we should've been able to pay with the material amount
- gap = Amounts.zeroOfCurrency(currency);
}
- perExchange[exch.baseUrl] = {
- balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
- feeGapEstimate: Amounts.stringify(gap),
- };
-
- maxFeeGapEstimate = Amounts.max(maxFeeGapEstimate, gap);
}
-
- const errDetails: PayPeerInsufficientBalanceDetails = {
- amountRequested: Amounts.stringify(instructedAmount),
- balanceAvailable: Amounts.stringify(infoGeneral.balanceAvailable),
- balanceMaterial: Amounts.stringify(infoGeneral.balanceMaterial),
- feeGapEstimate: Amounts.stringify(maxFeeGapEstimate),
- perExchange,
+ const insufficientBalanceDetails = await reportInsufficientBalanceDetails(
+ wex,
+ tx,
+ {
+ restrictExchanges: undefined,
+ instructedAmount: req.instructedAmount,
+ requiredMinimumAge: undefined,
+ wireMethod: undefined,
+ },
+ );
+ return {
+ type: "failure",
+ insufficientBalanceDetails,
};
-
- return { type: "failure", insufficientBalanceDetails: errDetails };
},
);
}
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index 960b123c6..2e28ba9b7 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -72,7 +72,7 @@ import {
stringToBytes,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
-import { selectPayCoinsNew } from "./coinSelection.js";
+import { selectPayCoins } from "./coinSelection.js";
import {
PendingTaskType,
TaskIdStr,
@@ -1219,10 +1219,12 @@ export async function prepareDepositGroup(
"",
);
- const payCoinSel = await selectPayCoinsNew(wex, {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -1338,10 +1340,12 @@ export async function createDepositGroup(
"",
);
- const payCoinSel = await selectPayCoinsNew(wex, {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
+ const payCoinSel = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
index a3623e6d2..ed58dc404 100644
--- a/packages/taler-wallet-core/src/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -96,7 +96,7 @@ import {
readUnexpectedResponseDetails,
throwUnexpectedRequestError,
} from "@gnu-taler/taler-util/http";
-import { PreviousPayCoins, selectPayCoinsNew } from "./coinSelection.js";
+import { PreviousPayCoins, selectPayCoins } from "./coinSelection.js";
import {
constructTaskIdentifier,
PendingTaskType,
@@ -1161,10 +1161,12 @@ async function handleInsufficientFunds(
}
});
- const res = await selectPayCoinsNew(wex, {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
+ const res = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
@@ -1285,16 +1287,18 @@ async function checkPaymentByProposalId(
purchase.purchaseStatus === PurchaseStatus.DialogShared
) {
// If not already paid, check if we could pay for it.
- const res = await selectPayCoinsNew(wex, {
- auditors: [],
- exchanges: contractData.allowedExchanges,
+ const res = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
wireFeeLimit: Amounts.parseOrThrow(contractData.maxWireFee),
prevPayCoins: [],
requiredMinimumAge: contractData.minimumAge,
- wireMethod: contractData.wireMethod,
+ restrictWireMethod: contractData.wireMethod,
});
if (res.type !== "success") {
@@ -1820,10 +1824,12 @@ export async function confirmPay(
const contractData = d.contractData;
- const selectCoinsResult = await selectPayCoinsNew(wex, {
- auditors: [],
- exchanges: contractData.allowedExchanges,
- wireMethod: contractData.wireMethod,
+ const selectCoinsResult = await selectPayCoins(wex, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractData.allowedExchanges,
+ },
+ restrictWireMethod: contractData.wireMethod,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,