commit d3695a93ffdc94853c317e8a05aed7c077c00623
parent 25c38a8498917d52865e40145a8c11cbfa84c1b2
Author: Florian Dold <florian@dold.me>
Date: Tue, 18 Feb 2025 12:27:07 +0100
wallet-core: implement insufficient balance cause hint
Diffstat:
2 files changed, 79 insertions(+), 4 deletions(-)
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -765,7 +765,7 @@ export enum InsufficientBalanceHint {
* the age restriction on coins causes the spendable
* balance to be insufficient.
*/
- MerchantAgeRestricted = "merchant-age-restricted",
+ AgeRestricted = "age-restricted",
/**
* While in principle the balance is sufficient,
@@ -779,13 +779,22 @@ export enum InsufficientBalanceHint {
* but the material funds are insufficient. Usually because there is a
* pending refresh operation.
*/
- WalletMateriallyInsufficient = "wallet-materially-insufficient",
+ WalletBalanceMaterialInsufficient = "wallet-balance-material-insufficient",
+
+ WalletBalanceAvailableInsufficient = "wallet-balance-available-insufficient",
/**
* Exchange is missing the global fee configuration, thus fees are unknown
* and funds from this exchange can't be used for p2p payments.
*/
ExchangeMissingGlobalFees = "exchange-missing-global-fees",
+
+ /**
+ * Even though the balance looks sufficient for the instructed amount,
+ * the fees can be covered by neither the merchant nor the remaining wallet
+ * balance.
+ */
+ FeesNotCovered = "fees-not-covered",
}
/**
@@ -798,6 +807,21 @@ export interface PaymentInsufficientBalanceDetails {
amountRequested: AmountString;
/**
+ * Wire method for the requested payment, only applicable
+ * for merchant payments.
+ */
+ wireMethod?: string | undefined;
+
+ /**
+ * Hint as to why the balance is insufficient.
+ *
+ * If this hint is not provided, the balance hints of
+ * the individual exchanges should be shown, as the overall
+ * reason might be a combination of the reasons for different exchanges.
+ */
+ causeHint?: InsufficientBalanceHint;
+
+ /**
* Balance of type "available" (see balance.ts for definition).
*/
balanceAvailable: AmountString;
@@ -844,7 +868,7 @@ export interface PaymentInsufficientBalanceDetails {
* Exchange doesn't have global fees configured for the relevant year,
* p2p payments aren't possible.
*
- * @deprecated use causeHint instead
+ * @deprecated (2025-02-18) use causeHint instead
*/
missingGlobalFees: boolean;
@@ -861,6 +885,7 @@ export const codecForPayMerchantInsufficientBalanceDetails =
(): Codec<PaymentInsufficientBalanceDetails> =>
buildCodecForObject<PaymentInsufficientBalanceDetails>()
.property("amountRequested", codecForAmountString())
+ .property("wireMethod", codecOptional(codecForString()))
.property("balanceAgeAcceptable", codecForAmountString())
.property("balanceAvailable", codecForAmountString())
.property("balanceMaterial", codecForAmountString())
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -43,6 +43,7 @@ import {
GetMaxDepositAmountResponse,
GetMaxPeerPushDebitAmountRequest,
GetMaxPeerPushDebitAmountResponse,
+ InsufficientBalanceHint,
j2s,
Logger,
parsePaytoUri,
@@ -56,7 +57,10 @@ import {
strcmp,
TalerProtocolTimestamp,
} from "@gnu-taler/taler-util";
-import { getPaymentBalanceDetailsInTx } from "./balance.js";
+import {
+ getPaymentBalanceDetailsInTx,
+ PaymentBalanceDetails,
+} from "./balance.js";
import { getAutoRefreshExecuteThreshold } from "./common.js";
import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js";
import {
@@ -498,6 +502,48 @@ interface ReportInsufficientBalanceRequest {
depositPaytoUri: string | undefined;
}
+function getHint(
+ req: ReportInsufficientBalanceRequest,
+ exchDet: PaymentBalanceDetails,
+): InsufficientBalanceHint | undefined {
+ if (Amounts.cmp(exchDet.balanceAvailable, req.instructedAmount) >= 0) {
+ // It looks to the user like funds at this exchange should be sufficient.
+ // We now need to explain why that's not the case.
+
+ const isMerchant = !!req.wireMethod;
+
+ if (Amounts.cmp(exchDet.balanceMaterial, req.instructedAmount) <= 0) {
+ return InsufficientBalanceHint.WalletBalanceMaterialInsufficient;
+ } else if (
+ Amounts.cmp(exchDet.balanceAgeAcceptable, req.instructedAmount) < 0
+ ) {
+ return InsufficientBalanceHint.AgeRestricted;
+ } else if (
+ isMerchant &&
+ Amounts.cmp(exchDet.balanceReceiverAcceptable, req.instructedAmount)
+ ) {
+ return InsufficientBalanceHint.MerchantAcceptInsufficient;
+ } else if (
+ isMerchant &&
+ Amounts.cmp(exchDet.balanceExchangeDepositable, req.instructedAmount)
+ ) {
+ return InsufficientBalanceHint.MerchantDepositInsufficient;
+ } else if (
+ isMerchant &&
+ Amounts.cmp(
+ exchDet.maxMerchantEffectiveDepositAmount,
+ req.instructedAmount,
+ ) < 0
+ ) {
+ return InsufficientBalanceHint.FeesNotCovered;
+ }
+ // We don't exactly know why the balance is insufficient!
+ return undefined;
+ }
+ // Simply not enough funds.
+ return InsufficientBalanceHint.WalletBalanceAvailableInsufficient;
+}
+
export async function reportInsufficientBalanceDetails(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<
@@ -554,6 +600,7 @@ export async function reportInsufficientBalanceDetails(
minAge: req.requiredMinimumAge ?? 0,
depositPaytoUri: req.depositPaytoUri,
});
+
perExchange[exch.baseUrl] = {
balanceAvailable: Amounts.stringify(exchDet.balanceAvailable),
balanceMaterial: Amounts.stringify(exchDet.balanceMaterial),
@@ -571,11 +618,14 @@ export async function reportInsufficientBalanceDetails(
exchDet.maxMerchantEffectiveDepositAmount,
),
missingGlobalFees,
+ causeHint: getHint(req, exchDet),
};
}
return {
amountRequested: Amounts.stringify(req.instructedAmount),
+ wireMethod: req.wireMethod,
+ causeHint: getHint(req, details),
balanceAgeAcceptable: Amounts.stringify(details.balanceAgeAcceptable),
balanceAvailable: Amounts.stringify(details.balanceAvailable),
balanceMaterial: Amounts.stringify(details.balanceMaterial),