aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/pay-merchant.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2023-01-05 18:45:49 +0100
committerFlorian Dold <florian@dold.me>2023-01-05 18:45:54 +0100
commit92f1b5928c764b3af12a29b97bbc3e434a82b1b0 (patch)
tree040f88aa54aec8fedb99ba57ad18218715d19e25 /packages/taler-wallet-core/src/operations/pay-merchant.ts
parent44aaa7a636ba25b37c1c26a306e64e0db75a2747 (diff)
downloadwallet-core-92f1b5928c764b3af12a29b97bbc3e434a82b1b0.tar.xz
wallet-core: implement insufficient balance details
For now, only for merchant payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations/pay-merchant.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts99
1 files changed, 73 insertions, 26 deletions
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<string, AmountJson> = {};
@@ -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<PayCoinSelection | undefined> {
+): Promise<SelectPayCoinsResult> {
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,
);