summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/coinSelection.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/coinSelection.ts')
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts366
1 files changed, 167 insertions, 199 deletions
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index 1208e7c37..5ac52e1d3 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -27,19 +27,15 @@ import { GlobalIDB } from "@gnu-taler/idb-bridge";
import {
AbsoluteTime,
AccountRestriction,
- AgeCommitmentProof,
AgeRestriction,
AllowedAuditorInfo,
AllowedExchangeInfo,
AmountJson,
Amounts,
- AmountString,
checkDbInvariant,
checkLogicInvariant,
- CoinPublicKeyString,
CoinStatus,
DenominationInfo,
- Duration,
ForcedCoinSel,
InternationalizedString,
j2s,
@@ -47,9 +43,9 @@ import {
parsePaytoUri,
PayCoinSelection,
PayMerchantInsufficientBalanceDetails,
+ SelectedCoin,
strcmp,
TalerProtocolTimestamp,
- UnblindedSignature,
} from "@gnu-taler/taler-util";
import {
getExchangePaymentBalanceDetailsInTx,
@@ -68,8 +64,6 @@ const logger = new Logger("coinSelection.ts");
export type PreviousPayCoins = {
coinPub: string;
contribution: AmountJson;
- feeDeposit: AmountJson;
- exchangeBaseUrl: string;
}[];
export interface ExchangeRestrictionSpec {
@@ -192,12 +186,7 @@ export async function selectPayCoins(
wex: WalletExecutionContext,
req: SelectPayCoinRequestNg,
): Promise<SelectPayCoinsResult> {
- const {
- contractTermsAmount,
- depositFeeLimit,
- wireFeeLimit,
- wireFeeAmortization,
- } = req;
+ const { contractTermsAmount, depositFeeLimit, wireFeeLimit } = req;
return await wex.db.runReadOnlyTx(
[
@@ -221,8 +210,7 @@ export async function selectPayCoins(
},
);
- const coinPubs: string[] = [];
- const coinContributions: AmountJson[] = [];
+ const coinRes: SelectedCoin[] = [];
const currency = contractTermsAmount.currency;
let tally: CoinSelectionTally = {
@@ -235,25 +223,17 @@ export async function selectPayCoins(
lastDepositFee: Amounts.zeroOfCurrency(currency),
};
- 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;
-
- coinPubs.push(prev.coinPub);
- coinContributions.push(prev.contribution);
- }
+ await maybeRepairCoinSelection(
+ wex,
+ tx,
+ req.prevPayCoins ?? [],
+ coinRes,
+ tally,
+ {
+ wireFeeAmortization: req.wireFeeAmortization,
+ wireFeesPerExchange: wireFeesPerExchange,
+ },
+ );
let selectedDenom: SelResult | undefined;
if (req.forcedSelection) {
@@ -292,8 +272,7 @@ export async function selectPayCoins(
const coinSel = await assembleSelectPayCoinsSuccessResult(
tx,
selectedDenom,
- coinPubs,
- coinContributions,
+ coinRes,
req.contractTermsAmount,
tally,
);
@@ -306,11 +285,55 @@ export async function selectPayCoins(
);
}
+async function maybeRepairCoinSelection(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
+ prevPayCoins: PreviousPayCoins,
+ coinRes: SelectedCoin[],
+ tally: CoinSelectionTally,
+ feeInfo: {
+ wireFeeAmortization: number;
+ wireFeesPerExchange: Record<string, AmountJson>;
+ },
+): Promise<void> {
+ // Look at existing pay coin selection and tally up
+ for (const prev of prevPayCoins) {
+ const coin = await tx.coins.get(prev.coinPub);
+ if (!coin) {
+ continue;
+ }
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ coin.exchangeBaseUrl,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
+ tallyFees(
+ tally,
+ feeInfo.wireFeesPerExchange,
+ feeInfo.wireFeeAmortization,
+ coin.exchangeBaseUrl,
+ Amounts.parseOrThrow(denom.feeDeposit),
+ );
+ tally.amountPayRemaining = Amounts.sub(
+ tally.amountPayRemaining,
+ prev.contribution,
+ ).amount;
+
+ coinRes.push({
+ coinPub: prev.coinPub,
+ contribution: Amounts.stringify(prev.contribution),
+ });
+ }
+}
+
async function assembleSelectPayCoinsSuccessResult(
tx: WalletDbReadOnlyTransaction<["coins"]>,
finalSel: SelResult,
- coinPubs: string[],
- coinContributions: AmountJson[],
+ coinRes: SelectedCoin[],
contractTermsAmount: AmountJson,
tally: CoinSelectionTally,
): Promise<PayCoinSelection> {
@@ -334,15 +357,17 @@ async function assembleSelectPayCoinsSuccessResult(
`coin selection failed (not available anymore, got only ${coins.length}/${numRequested})`,
);
}
- coinPubs.push(...coins.map((x) => x.coinPub));
- coinContributions.push(...selInfo.contributions);
+
+ for (let i = 0; i < selInfo.contributions.length; i++) {
+ coinRes.push({
+ coinPub: coins[i].coinPub,
+ contribution: Amounts.stringify(selInfo.contributions[i]),
+ });
+ }
}
return {
- // FIXME: Why do we return this?!
- paymentAmount: Amounts.stringify(contractTermsAmount),
- coinContributions: coinContributions.map((x) => Amounts.stringify(x)),
- coinPubs,
+ coins: coinRes,
customerDepositFees: Amounts.stringify(tally.customerDepositFees),
customerWireFees: Amounts.stringify(tally.customerWireFees),
};
@@ -358,7 +383,13 @@ interface ReportInsufficientBalanceRequest {
export async function reportInsufficientBalanceDetails(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<
- ["coinAvailability", "exchanges", "exchangeDetails", "refreshGroups"]
+ [
+ "coinAvailability",
+ "exchanges",
+ "exchangeDetails",
+ "refreshGroups",
+ "denominations",
+ ]
>,
req: ReportInsufficientBalanceRequest,
): Promise<PayMerchantInsufficientBalanceDetails> {
@@ -384,10 +415,52 @@ export async function reportInsufficientBalanceDetails(
const exchanges = await tx.exchanges.iter().toArray();
+ let maxEffectiveSpendAmount = Amounts.zeroOfAmount(req.instructedAmount);
+
for (const exch of exchanges) {
if (exch.detailsPointer?.currency !== currency) {
continue;
}
+
+ // We now see how much we could spend if we paid all the fees ourselves
+ // in a worst-case estimate.
+
+ const exchangeBaseUrl = exch.baseUrl;
+ 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(
+ [exchangeBaseUrl, ageLower, 1],
+ [exchangeBaseUrl, ageUpper, Number.MAX_SAFE_INTEGER],
+ ),
+ );
+
+ for (const ec of myExchangeCoins) {
+ maxEffectiveSpendAmount = Amounts.add(
+ maxEffectiveSpendAmount,
+ Amounts.mult(ec.value, ec.freshCoinCount).amount,
+ ).amount;
+
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ exchangeBaseUrl,
+ ec.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
+ maxEffectiveSpendAmount = Amounts.sub(
+ maxEffectiveSpendAmount,
+ Amounts.mult(denom.feeDeposit, ec.freshCoinCount).amount,
+ ).amount;
+ }
+
const infoExchange = await getExchangePaymentBalanceDetailsInTx(wex, tx, {
currency,
restrictExchangeTo: exch.baseUrl,
@@ -395,7 +468,6 @@ export async function reportInsufficientBalanceDetails(
perExchange[exch.baseUrl] = {
balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable),
balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial),
- feeGapEstimate: Amounts.stringify(Amounts.zeroOfCurrency(currency)),
};
}
@@ -410,7 +482,7 @@ export async function reportInsufficientBalanceDetails(
balanceMerchantDepositable: Amounts.stringify(
details.balanceMerchantDepositable,
),
- feeGapEstimate: Amounts.stringify(feeGapEstimate),
+ maxEffectiveSpendAmount: Amounts.stringify(maxEffectiveSpendAmount),
perExchange,
};
}
@@ -434,8 +506,6 @@ interface SelResult {
[avKey: string]: {
exchangeBaseUrl: string;
denomPubHash: string;
- expireWithdraw: TalerProtocolTimestamp;
- expireDeposit: TalerProtocolTimestamp;
maxAge: number;
contributions: AmountJson[];
};
@@ -508,8 +578,6 @@ function selectGreedy(
denomPubHash: denom.denomPubHash,
exchangeBaseUrl: denom.exchangeBaseUrl,
maxAge: denom.maxAge,
- expireDeposit: denom.stampExpireDeposit,
- expireWithdraw: denom.stampExpireWithdraw,
};
}
sd.contributions.push(...contributions);
@@ -549,8 +617,6 @@ function selectForced(
denomPubHash: aci.denomPubHash,
exchangeBaseUrl: aci.exchangeBaseUrl,
maxAge: aci.maxAge,
- expireDeposit: aci.stampExpireDeposit,
- expireWithdraw: aci.stampExpireWithdraw,
};
}
sd.contributions.push(Amounts.parseOrThrow(forcedCoin.value));
@@ -563,7 +629,6 @@ function selectForced(
throw Error("can't find coin for forced coin selection");
}
}
-
return selectedDenom;
}
@@ -696,12 +761,7 @@ interface SelectPayCandidatesRequest {
instructedAmount: AmountJson;
restrictWireMethod: string | undefined;
depositPaytoUri?: string;
- restrictExchanges:
- | {
- exchanges: AllowedExchangeInfo[];
- auditors: AllowedAuditorInfo[];
- }
- | undefined;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
requiredMinimumAge?: number;
}
@@ -796,36 +856,13 @@ async function selectPayCandidates(
return [denoms, wfPerExchange];
}
-export interface CoinInfo {
- id: string;
- value: AmountJson;
- denomDeposit: AmountJson;
- denomWithdraw: AmountJson;
- denomRefresh: AmountJson;
- totalAvailable: number | undefined;
- exchangeWire: AmountJson | undefined;
- exchangePurse: AmountJson | undefined;
- duration: Duration;
- exchangeBaseUrl: string;
- maxAge: number;
-}
-
-export interface SelectedPeerCoin {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
-}
-
export interface PeerCoinSelectionDetails {
exchangeBaseUrl: string;
/**
* Info of Coins that were selected.
*/
- coins: SelectedPeerCoin[];
+ coins: SelectedCoin[];
/**
* How much of the deposit fees is the customer paying?
@@ -842,12 +879,6 @@ export type SelectPeerCoinsResult =
insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails;
};
-export interface PeerCoinRepair {
- exchangeBaseUrl: string;
- coinPubs: CoinPublicKeyString[];
- contribs: AmountJson[];
-}
-
export interface PeerCoinSelectionRequest {
instructedAmount: AmountJson;
@@ -855,121 +886,39 @@ export interface PeerCoinSelectionRequest {
* Instruct the coin selection to repair this coin
* selection instead of selecting completely new coins.
*/
- repair?: PeerCoinRepair;
+ repair?: PreviousPayCoins;
}
-async function assemblePeerCoinSelectionDetails(
- tx: WalletDbReadOnlyTransaction<["coins"]>,
- exchangeBaseUrl: string,
+export async function computeCoinSelMaxExpirationDate(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<["coins", "denominations"]>,
selectedDenom: SelResult,
- resCoins: ResCoin[],
- tally: CoinSelectionTally,
-): Promise<PeerCoinSelectionDetails> {
+): Promise<TalerProtocolTimestamp> {
let minAutorefreshExecuteThreshold = TalerProtocolTimestamp.never();
for (const dph of Object.keys(selectedDenom)) {
const selInfo = selectedDenom[dph];
+ const denom = await getDenomInfo(
+ wex,
+ tx,
+ selInfo.exchangeBaseUrl,
+ selInfo.denomPubHash,
+ );
+ if (!denom) {
+ continue;
+ }
// 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,
+ stampExpireDeposit: denom.stampExpireDeposit,
+ stampExpireWithdraw: denom.stampExpireWithdraw,
}),
),
);
- 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,
- });
- }
}
-
- return {
- exchangeBaseUrl,
- coins: resCoins,
- depositFees: tally.customerDepositFees,
- maxExpirationDate: minAutorefreshExecuteThreshold,
- };
-}
-
-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,
- );
- 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;
- }
- }
- return resCoins;
-}
-
-interface ResCoin {
- coinPub: string;
- coinPriv: string;
- contribution: AmountString;
- denomPubHash: string;
- denomSig: UnblindedSignature;
- ageCommitmentProof: AgeCommitmentProof | undefined;
+ return minAutorefreshExecuteThreshold;
}
export function emptyTallyForPeerPayment(
@@ -1034,12 +983,18 @@ export async function selectPeerCoins(
logger.trace(`peer payment candidate coins: ${j2s(candidates)}`);
}
const tally = emptyTallyForPeerPayment(req.instructedAmount);
- const resCoins: ResCoin[] = await maybeRepairPeerCoinSelection(
+ const resCoins: SelectedCoin[] = [];
+
+ await maybeRepairCoinSelection(
wex,
tx,
- exch.baseUrl,
+ req.repair ?? [],
+ resCoins,
tally,
- req.repair,
+ {
+ wireFeeAmortization: 1,
+ wireFeesPerExchange: {},
+ },
);
if (logger.shouldLogTrace()) {
@@ -1058,15 +1013,28 @@ export async function selectPeerCoins(
);
if (selectedDenom) {
+ const r = await assembleSelectPayCoinsSuccessResult(
+ tx,
+ selectedDenom,
+ resCoins,
+ req.instructedAmount,
+ tally,
+ );
+
+ const maxExpirationDate = await computeCoinSelMaxExpirationDate(
+ wex,
+ tx,
+ selectedDenom,
+ );
+
return {
type: "success",
- result: await assemblePeerCoinSelectionDetails(
- tx,
- exch.baseUrl,
- selectedDenom,
- resCoins,
- tally,
- ),
+ result: {
+ coins: r.coins,
+ depositFees: Amounts.parseOrThrow(r.customerDepositFees),
+ exchangeBaseUrl: exch.baseUrl,
+ maxExpirationDate,
+ },
};
}
}