From 92f1b5928c764b3af12a29b97bbc3e434a82b1b0 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 5 Jan 2023 18:45:49 +0100 Subject: wallet-core: implement insufficient balance details For now, only for merchant payments --- .../src/operations/pay-merchant.ts | 99 ++++++++++++++++------ 1 file changed, 73 insertions(+), 26 deletions(-) (limited to 'packages/taler-wallet-core/src/operations/pay-merchant.ts') diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 05da0a020..6026e0860 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -73,6 +73,7 @@ import { TransactionType, URL, constructPayUri, + PayMerchantInsufficientBalanceDetails, } from "@gnu-taler/taler-util"; import { EddsaKeypair } from "../crypto/cryptoImplementation.js"; import { @@ -131,11 +132,12 @@ import { import { getExchangeDetails } from "./exchanges.js"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js"; import { GetReadOnlyAccess } from "../util/query.js"; +import { getMerchantPaymentBalanceDetails } from "./balance.js"; /** * Logger. */ -const logger = new Logger("pay.ts"); +const logger = new Logger("pay-merchant.ts"); /** * Compute the total cost of a payment to the customer. @@ -817,7 +819,7 @@ async function handleInsufficientFunds( requiredMinimumAge: contractData.minimumAge, }); - if (!res) { + if (res.type !== "success") { logger.trace("insufficient funds for coin re-selection"); return; } @@ -841,8 +843,7 @@ async function handleInsufficientFunds( if (!payInfo) { return; } - payInfo.payCoinSelection = res; - payInfo.payCoinSelection = res; + payInfo.payCoinSelection = res.coinSel; payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); await tx.purchases.put(p); await spendCoins(ws, tx, { @@ -905,6 +906,8 @@ export async function selectCandidates( x.coinAvailability, ]) .runReadOnly(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 = {}; @@ -1030,6 +1033,7 @@ export function selectGreedy( // 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.value) > 0) { + tally.lastDepositFee = Amounts.parseOrThrow(aci.feeDeposit); continue; } @@ -1129,6 +1133,13 @@ export function selectForced( return selectedDenom; } +export type SelectPayCoinsResult = + | { + type: "failure"; + insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + } + | { type: "success"; coinSel: PayCoinSelection }; + /** * Given a list of candidate coins, select coins to spend under the merchant's * constraints. @@ -1142,7 +1153,7 @@ export function selectForced( export async function selectPayCoinsNew( ws: InternalWalletState, req: SelectPayCoinRequestNg, -): Promise { +): Promise { const { contractTermsAmount, depositFeeLimit, @@ -1168,6 +1179,7 @@ export async function selectPayCoinsNew( customerDepositFees: Amounts.zeroOfCurrency(currency), customerWireFees: Amounts.zeroOfCurrency(currency), wireFeeCoveredForExchange: new Set(), + lastDepositFee: Amounts.zeroOfCurrency(currency), }; const prevPayCoins = req.prevPayCoins ?? []; @@ -1207,7 +1219,44 @@ export async function selectPayCoinsNew( } if (!selectedDenom) { - return undefined; + const details = await getMerchantPaymentBalanceDetails(ws, { + 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); + } + 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 finalSel = selectedDenom; @@ -1244,11 +1293,14 @@ export async function selectPayCoinsNew( }); return { - paymentAmount: Amounts.stringify(contractTermsAmount), - coinContributions: coinContributions.map((x) => Amounts.stringify(x)), - coinPubs, - customerDepositFees: Amounts.stringify(tally.customerDepositFees), - customerWireFees: Amounts.stringify(tally.customerWireFees), + 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), + }, }; } @@ -1318,7 +1370,7 @@ export async function checkPaymentByProposalId( wireMethod: contractData.wireMethod, }); - if (!res) { + if (res.type !== "success") { logger.info("not allowing payment, insufficient coins"); return { status: PreparePayResultType.InsufficientBalance, @@ -1327,10 +1379,11 @@ export async function checkPaymentByProposalId( noncePriv: proposal.noncePriv, amountRaw: Amounts.stringify(d.contractData.amount), talerUri, + balanceDetails: res.insufficientBalanceDetails, }; } - const totalCost = await getTotalPaymentCost(ws, res); + const totalCost = await getTotalPaymentCost(ws, res.coinSel); logger.trace("costInfo", totalCost); logger.trace("coinsForPayment", res); @@ -1340,7 +1393,7 @@ export async function checkPaymentByProposalId( proposalId: proposal.proposalId, noncePriv: proposal.noncePriv, amountEffective: Amounts.stringify(totalCost), - amountRaw: Amounts.stringify(res.paymentAmount), + amountRaw: Amounts.stringify(res.coinSel.paymentAmount), contractTermsHash: d.contractData.contractTermsHash, talerUri, }; @@ -1666,9 +1719,9 @@ export async function confirmPay( const contractData = d.contractData; - let maybeCoinSelection: PayCoinSelection | undefined = undefined; + let selectCoinsResult: SelectPayCoinsResult | undefined = undefined; - maybeCoinSelection = await selectPayCoinsNew(ws, { + selectCoinsResult = await selectPayCoinsNew(ws, { auditors: contractData.allowedAuditors, exchanges: contractData.allowedExchanges, wireMethod: contractData.wireMethod, @@ -1681,9 +1734,9 @@ export async function confirmPay( forcedSelection: forcedCoinSel, }); - logger.trace("coin selection result", maybeCoinSelection); + logger.trace("coin selection result", selectCoinsResult); - if (!maybeCoinSelection) { + if (selectCoinsResult.type === "failure") { // Should not happen, since checkPay should be called first // FIXME: Actually, this should be handled gracefully, // and the status should be stored in the DB. @@ -1691,14 +1744,7 @@ export async function confirmPay( throw Error("insufficient balance"); } - const coinSelection = maybeCoinSelection; - - const depositPermissions = await generateDepositPermissions( - ws, - coinSelection, - d.contractData, - ); - + const coinSelection = selectCoinsResult.coinSel; const payCostInfo = await getTotalPaymentCost(ws, coinSelection); let sessionId: string | undefined; @@ -2373,6 +2419,7 @@ async function acceptRefunds( await createRefreshGroup( ws, tx, + Amounts.currencyOf(refreshCoinsPubs[0].amount), refreshCoinsPubs, RefreshReason.Refund, ); -- cgit v1.2.3