taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/types-taler-wallet.ts | 31++++++++++++++++++++++++++++---
Mpackages/taler-wallet-core/src/coinSelection.ts | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
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),