From 466e2b7643692aa6b7f76a193b84775008e17350 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 7 Mar 2024 11:10:52 +0100 Subject: wallet-core: improve insufficient balance reporting --- packages/taler-harness/src/harness/harness.ts | 62 +++--- .../test-wallet-insufficient-balance.ts | 166 ++++++++++++++++ .../src/integrationtests/testrunner.ts | 2 + packages/taler-util/src/errors.ts | 6 +- packages/taler-util/src/wallet-types.ts | 14 +- packages/taler-wallet-core/src/balance.ts | 212 ++++++++------------- packages/taler-wallet-core/src/coinSelection.ts | 114 ++++------- packages/taler-wallet-core/src/wallet-api-types.ts | 4 +- .../src/components/PaymentButtons.tsx | 4 +- 9 files changed, 342 insertions(+), 242 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts (limited to 'packages') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 291cd4c6d..3d9e7fb8a 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -605,6 +605,11 @@ export interface HarnessExchangeBankAccount { debitRestrictions?: AccountRestriction[]; creditRestrictions?: AccountRestriction[]; + + /** + * If set, the harness will not automatically configure the wire fee for this account. + */ + skipWireFeeCreation?: boolean; } /** @@ -1320,7 +1325,6 @@ export class ExchangeService implements ExchangeServiceInterface { if (!p) { throw Error(`invalid payto uri in exchange config: ${paytoUri}`); } - accountTargetTypes.add(p?.targetType); const optArgs: string[] = []; if (acct.conversionUrl != null) { optArgs.push("conversion-url", acct.conversionUrl); @@ -1339,33 +1343,43 @@ export class ExchangeService implements ExchangeServiceInterface { "upload", ], ); - } - const year = new Date().getFullYear(); - for (const accTargetType of accountTargetTypes.values()) { - for (let i = year; i < year + 5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "wire-fee", - // Year - `${i}`, - // Wire method - accTargetType, - // Wire fee - `${this.exchangeConfig.currency}:0.01`, - // Closing fee - `${this.exchangeConfig.currency}:0.01`, - "upload", - ], - ); + const accTargetType = p.targetType; + + const covered = accountTargetTypes.has(p.targetType); + if (!covered && !acct.skipWireFeeCreation) { + const year = new Date().getFullYear(); + + for (let i = year; i < year + 5; i++) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "wire-fee", + // Year + `${i}`, + // Wire method + accTargetType, + // Wire fee + `${this.exchangeConfig.currency}:0.01`, + // Closing fee + `${this.exchangeConfig.currency}:0.01`, + "upload", + ], + ); + accountTargetTypes.add(accTargetType); + } } } + const wireTypeConfigured: Set = new Set(); + + for (const acct of this.exchangeBankAccounts) { + } + await runCommand( this.globalState, "exchange-offline", diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts new file mode 100644 index 000000000..ac1244446 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts @@ -0,0 +1,166 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + AmountString, + Duration, + PaymentInsufficientBalanceDetails, + TalerErrorCode, + WalletNotification, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + ExchangeService, + FakebankService, + GlobalTestState, + MerchantService, + WalletClient, + WalletService, + generateRandomPayto, + setupDb, +} from "../harness/harness.js"; +import { withdrawViaBankV2 } from "../harness/helpers.js"; + +export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const bank = await FakebankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "x", + ); + exchangeBankAccount.skipWireFeeCreation = true; + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); + exchange.addCoinConfigList(coinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [generateRandomPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [generateRandomPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const allNotifications: WalletNotification[] = []; + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + allNotifications.push(n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + const wres = await withdrawViaBankV2(t, { + amount: "TESTKUDOS:10", + bank, + exchange, + walletClient, + }); + await wres.withdrawalFinishedCond; + + const exc = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.PrepareDeposit, { + amount: "TESTKUDOS:5" as AmountString, + depositPaytoUri: "payto://x-taler-bank/localhost/foobar", + }); + }); + t.assertDeepEqual( + exc.errorDetail.code, + TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + ); + const insufficientBalanceDetails: PaymentInsufficientBalanceDetails = + exc.errorDetail.insufficientBalanceDetails; + + t.assertAmountEquals( + insufficientBalanceDetails.balanceAvailable, + "TESTKUDOS:9.72", + ); + t.assertAmountEquals( + insufficientBalanceDetails.balanceExchangeDepositable, + "TESTKUDOS:0", + ); +} + +runWalletInsufficientBalanceTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 803e68e6b..0bfb245ab 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -96,6 +96,7 @@ import { runWalletDblessTest } from "./test-wallet-dbless.js"; import { runWalletDd48Test } from "./test-wallet-dd48.js"; import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js"; import { runWalletGenDbTest } from "./test-wallet-gendb.js"; +import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js"; import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWalletObservabilityTest } from "./test-wallet-observability.js"; import { runWalletRefreshTest } from "./test-wallet-refresh.js"; @@ -204,6 +205,7 @@ const allTests: TestMainFunction[] = [ runWalletObservabilityTest, runWalletDevExperimentsTest, runWalletBalanceZeroTest, + runWalletInsufficientBalanceTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts index c4733a194..3ada34d63 100644 --- a/packages/taler-util/src/errors.ts +++ b/packages/taler-util/src/errors.ts @@ -25,7 +25,7 @@ */ import { AbsoluteTime, - PayMerchantInsufficientBalanceDetails, + PaymentInsufficientBalanceDetails, TalerErrorCode, TalerErrorDetail, TransactionType, @@ -132,10 +132,10 @@ export interface DetailsMap { kycUrl: string; }; [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: { - insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + insufficientBalanceDetails: PaymentInsufficientBalanceDetails; }; [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: { - insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + insufficientBalanceDetails: PaymentInsufficientBalanceDetails; }; [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: { numErrors: number; diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index cb4374648..8be8fc4a0 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -833,10 +833,12 @@ export const codecForPreparePayResultPaymentPossible = ) .build("PreparePayResultPaymentPossible"); +export interface BalanceDetails {} + /** * Detailed reason for why the wallet's balance is insufficient. */ -export interface PayMerchantInsufficientBalanceDetails { +export interface PaymentInsufficientBalanceDetails { /** * Amount requested by the merchant. */ @@ -867,6 +869,8 @@ export interface PayMerchantInsufficientBalanceDetails { */ balanceMerchantDepositable: AmountString; + balanceExchangeDepositable: AmountString; + /** * Maximum effective amount that the wallet can spend, * when all fees are paid by the wallet. @@ -877,19 +881,21 @@ export interface PayMerchantInsufficientBalanceDetails { [url: string]: { balanceAvailable: AmountString; balanceMaterial: AmountString; + balanceExchangeDepositable: AmountString; }; }; } export const codecForPayMerchantInsufficientBalanceDetails = - (): Codec => - buildCodecForObject() + (): Codec => + buildCodecForObject() .property("amountRequested", codecForAmountString()) .property("balanceAgeAcceptable", codecForAmountString()) .property("balanceAvailable", codecForAmountString()) .property("balanceMaterial", codecForAmountString()) .property("balanceMerchantAcceptable", codecForAmountString()) .property("balanceMerchantDepositable", codecForAmountString()) + .property("balanceExchangeDepositable", codecForAmountString()) .property("perExchange", codecForAny()) .property("maxEffectiveSpendAmount", codecForAmountString()) .build("PayMerchantInsufficientBalanceDetails"); @@ -981,7 +987,7 @@ export interface PreparePayResultInsufficientBalance { contractTerms: MerchantContractTerms; amountRaw: AmountString; talerUri: string; - balanceDetails: PayMerchantInsufficientBalanceDetails; + balanceDetails: PaymentInsufficientBalanceDetails; } export interface PreparePayResultAlreadyConfirmed { diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts index a77358363..4c58814a1 100644 --- a/packages/taler-wallet-core/src/balance.ts +++ b/packages/taler-wallet-core/src/balance.ts @@ -67,6 +67,7 @@ import { ScopeInfo, ScopeType, } from "@gnu-taler/taler-util"; +import { findMatchingWire } from "./coinSelection.js"; import { DepositOperationStatus, OPERATION_STATUS_ACTIVE_FIRST, @@ -80,7 +81,7 @@ import { getExchangeScopeInfo, getExchangeWireDetailsInTx, } from "./exchanges.js"; -import { WalletExecutionContext } from "./wallet.js"; +import { getDenomInfo, WalletExecutionContext } from "./wallet.js"; /** * Logger. @@ -460,14 +461,6 @@ export async function getBalances( return wbal; } -/** - * Information about the balance for a particular payment to a particular - * merchant. - */ -export interface MerchantPaymentBalanceDetails { - balanceAvailable: AmountJson; -} - export interface PaymentRestrictionsForBalance { currency: string; minAge: number; @@ -478,6 +471,7 @@ export interface PaymentRestrictionsForBalance { } | undefined; restrictWireMethods: string[] | undefined; + depositPaytoUri: string | undefined; } export interface AcceptableExchanges { @@ -587,7 +581,7 @@ async function getAcceptableExchangeBaseUrlsInTx( }; } -export interface MerchantPaymentBalanceDetails { +export interface PaymentBalanceDetails { /** * Balance of type "available" (see balance.ts for definition). */ @@ -612,46 +606,110 @@ export interface MerchantPaymentBalanceDetails { * Balance of type "merchant-depositable" (see balance.ts for definition). */ balanceMerchantDepositable: AmountJson; + + /** + * Balance that's depositable with the exchange. + * This balance is reduced by the exchange's debit restrictions + * and wire fee configuration. + */ + balanceExchangeDepositable: AmountJson; + + maxEffectiveSpendAmount: AmountJson; } -export async function getMerchantPaymentBalanceDetails( +export async function getPaymentBalanceDetails( wex: WalletExecutionContext, req: PaymentRestrictionsForBalance, -): Promise { +): Promise { return await wex.db.runReadOnlyTx( - ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"], + [ + "coinAvailability", + "refreshGroups", + "exchanges", + "exchangeDetails", + "denominations", + ], async (tx) => { - return getMerchantPaymentBalanceDetailsInTx(wex, tx, req); + return getPaymentBalanceDetailsInTx(wex, tx, req); }, ); } -export async function getMerchantPaymentBalanceDetailsInTx( +export async function getPaymentBalanceDetailsInTx( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction< - ["coinAvailability", "refreshGroups", "exchanges", "exchangeDetails"] + [ + "coinAvailability", + "refreshGroups", + "exchanges", + "exchangeDetails", + "denominations", + ] >, req: PaymentRestrictionsForBalance, -): Promise { +): Promise { const acceptability = await getAcceptableExchangeBaseUrlsInTx(wex, tx, req); - const d: MerchantPaymentBalanceDetails = { + const d: PaymentBalanceDetails = { balanceAvailable: Amounts.zeroOfCurrency(req.currency), balanceMaterial: Amounts.zeroOfCurrency(req.currency), balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency), balanceMerchantAcceptable: Amounts.zeroOfCurrency(req.currency), balanceMerchantDepositable: Amounts.zeroOfCurrency(req.currency), + maxEffectiveSpendAmount: Amounts.zeroOfCurrency(req.currency), + balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency), }; - await tx.coinAvailability.iter().forEach((ca) => { + const availableCoins = await tx.coinAvailability.getAll(); + + for (const ca of availableCoins) { if (ca.currency != req.currency) { - return; + continue; + } + + const denom = await getDenomInfo( + wex, + tx, + ca.exchangeBaseUrl, + ca.denomPubHash, + ); + if (!denom) { + continue; + } + + const wireDetails = await getExchangeWireDetailsInTx( + tx, + ca.exchangeBaseUrl, + ); + if (!wireDetails) { + continue; } + const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value); const coinAmount: AmountJson = Amounts.mult( singleCoinAmount, ca.freshCoinCount, ).amount; + + d.maxEffectiveSpendAmount = Amounts.add( + d.maxEffectiveSpendAmount, + Amounts.mult(ca.value, ca.freshCoinCount).amount, + ).amount; + + d.maxEffectiveSpendAmount = Amounts.sub( + d.maxEffectiveSpendAmount, + Amounts.mult(denom.feeDeposit, ca.freshCoinCount).amount, + ).amount; + + let wireOkay = false; + if (req.restrictWireMethods == null) { + wireOkay = true; + } else { + for (const wm of req.restrictWireMethods) { + const wmf = findMatchingWire(wm, req.depositPaytoUri, wireDetails); + } + } + d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount; d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount; if (ca.maxAge === 0 || ca.maxAge > req.minAge) { @@ -672,7 +730,7 @@ export async function getMerchantPaymentBalanceDetailsInTx( } } } - }); + } await tx.refreshGroups.iter().forEach((r) => { if (r.currency != req.currency) { @@ -690,7 +748,7 @@ export async function getMerchantPaymentBalanceDetailsInTx( export async function getBalanceDetail( wex: WalletExecutionContext, req: GetBalanceDetailRequest, -): Promise { +): Promise { const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = []; const wires = new Array(); await wex.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => { @@ -713,7 +771,7 @@ export async function getBalanceDetail( } }); - return await getMerchantPaymentBalanceDetails(wex, { + return await getPaymentBalanceDetails(wex, { currency: req.currency, restrictExchanges: { auditors: [], @@ -721,112 +779,6 @@ export async function getBalanceDetail( }, restrictWireMethods: wires, minAge: 0, + depositPaytoUri: undefined, }); } - -export interface PeerPaymentRestrictionsForBalance { - currency: string; - restrictExchangeTo?: string; -} - -export interface PeerPaymentBalanceDetails { - /** - * Balance of type "available" (see balance.ts for definition). - */ - balanceAvailable: AmountJson; - - /** - * Balance of type "material" (see balance.ts for definition). - */ - balanceMaterial: AmountJson; -} - -export async function getPeerPaymentBalanceDetailsInTx( - wex: WalletExecutionContext, - tx: WalletDbReadOnlyTransaction<["coinAvailability", "refreshGroups"]>, - req: PeerPaymentRestrictionsForBalance, -): Promise { - let balanceAvailable = Amounts.zeroOfCurrency(req.currency); - let balanceMaterial = Amounts.zeroOfCurrency(req.currency); - - await tx.coinAvailability.iter().forEach((ca) => { - if (ca.currency != req.currency) { - return; - } - if ( - req.restrictExchangeTo && - req.restrictExchangeTo !== ca.exchangeBaseUrl - ) { - return; - } - const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value); - const coinAmount: AmountJson = Amounts.mult( - singleCoinAmount, - ca.freshCoinCount, - ).amount; - balanceAvailable = Amounts.add(balanceAvailable, coinAmount).amount; - balanceMaterial = Amounts.add(balanceMaterial, coinAmount).amount; - }); - - await tx.refreshGroups.iter().forEach((r) => { - if (r.currency != req.currency) { - return; - } - balanceAvailable = Amounts.add( - balanceAvailable, - computeRefreshGroupAvailableAmount(r), - ).amount; - }); - - return { - balanceAvailable, - balanceMaterial, - }; -} - -/** - * Get information about the balance at a given exchange - * with certain restrictions. - */ -export async function getExchangePaymentBalanceDetailsInTx( - wex: WalletExecutionContext, - tx: WalletDbReadOnlyTransaction<["coinAvailability", "refreshGroups"]>, - req: PeerPaymentRestrictionsForBalance, -): Promise { - let balanceAvailable = Amounts.zeroOfCurrency(req.currency); - let balanceMaterial = Amounts.zeroOfCurrency(req.currency); - - await tx.coinAvailability.iter().forEach((ca) => { - if (ca.currency != req.currency) { - return; - } - if ( - req.restrictExchangeTo && - req.restrictExchangeTo !== ca.exchangeBaseUrl - ) { - return; - } - const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value); - const coinAmount: AmountJson = Amounts.mult( - singleCoinAmount, - ca.freshCoinCount, - ).amount; - balanceAvailable = Amounts.add(balanceAvailable, coinAmount).amount; - balanceMaterial = Amounts.add(balanceMaterial, coinAmount).amount; - }); - - await tx.refreshGroups.iter().forEach((r) => { - if (r.currency != req.currency) { - return; - } - balanceAvailable = Amounts.add( - balanceAvailable, - computeRefreshGroupAvailableAmount(r), - ).amount; - }); - - return { - balanceAvailable, - balanceMaterial, - }; -} diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index 5ac52e1d3..695be79ac 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -42,15 +42,12 @@ import { Logger, parsePaytoUri, PayCoinSelection, - PayMerchantInsufficientBalanceDetails, + PaymentInsufficientBalanceDetails, SelectedCoin, strcmp, TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; -import { - getExchangePaymentBalanceDetailsInTx, - getMerchantPaymentBalanceDetailsInTx, -} from "./balance.js"; +import { getPaymentBalanceDetailsInTx } from "./balance.js"; import { getAutoRefreshExecuteThreshold } from "./common.js"; import { DenominationRecord, WalletDbReadOnlyTransaction } from "./db.js"; import { @@ -171,7 +168,7 @@ function tallyFees( export type SelectPayCoinsResult = | { type: "failure"; - insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + insufficientBalanceDetails: PaymentInsufficientBalanceDetails; } | { type: "success"; coinSel: PayCoinSelection }; @@ -264,6 +261,7 @@ export async function selectPayCoins( instructedAmount: req.contractTermsAmount, requiredMinimumAge: req.requiredMinimumAge, wireMethod: req.restrictWireMethod, + depositPaytoUri: req.depositPaytoUri, }, ), } satisfies SelectPayCoinsResult; @@ -273,7 +271,6 @@ export async function selectPayCoins( tx, selectedDenom, coinRes, - req.contractTermsAmount, tally, ); @@ -334,7 +331,6 @@ async function assembleSelectPayCoinsSuccessResult( tx: WalletDbReadOnlyTransaction<["coins"]>, finalSel: SelResult, coinRes: SelectedCoin[], - contractTermsAmount: AmountJson, tally: CoinSelectionTally, ): Promise { for (const dph of Object.keys(finalSel)) { @@ -378,6 +374,7 @@ interface ReportInsufficientBalanceRequest { requiredMinimumAge: number | undefined; restrictExchanges: ExchangeRestrictionSpec | undefined; wireMethod: string | undefined; + depositPaytoUri: string | undefined; } export async function reportInsufficientBalanceDetails( @@ -392,82 +389,42 @@ export async function reportInsufficientBalanceDetails( ] >, req: ReportInsufficientBalanceRequest, -): Promise { - const currency = Amounts.currencyOf(req.instructedAmount); - const details = await getMerchantPaymentBalanceDetailsInTx(wex, tx, { +): Promise { + const details = await getPaymentBalanceDetailsInTx(wex, tx, { restrictExchanges: req.restrictExchanges, restrictWireMethods: req.wireMethod ? [req.wireMethod] : [], currency: Amounts.currencyOf(req.instructedAmount), minAge: req.requiredMinimumAge ?? 0, + depositPaytoUri: req.depositPaytoUri, }); - let feeGapEstimate: AmountJson; - - // FIXME: need fee gap estimate - // FIXME: We can probably give a better estimate. - // feeGapEstimate = Amounts.add( - // tally.amountPayRemaining, - // tally.lastDepositFee, - // ).amount; - - feeGapEstimate = Amounts.zeroOfAmount(req.instructedAmount); - - const perExchange: PayMerchantInsufficientBalanceDetails["perExchange"] = {}; - - const exchanges = await tx.exchanges.iter().toArray(); - - let maxEffectiveSpendAmount = Amounts.zeroOfAmount(req.instructedAmount); + const perExchange: PaymentInsufficientBalanceDetails["perExchange"] = {}; + const exchanges = await tx.exchanges.getAll(); for (const exch of exchanges) { - if (exch.detailsPointer?.currency !== currency) { + if (!exch.detailsPointer) { 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, + const exchDet = await getPaymentBalanceDetailsInTx(wex, tx, { + restrictExchanges: { + exchanges: [ + { + exchangeBaseUrl: exch.baseUrl, + exchangePub: exch.detailsPointer?.masterPublicKey, + }, + ], + auditors: [], + }, + restrictWireMethods: req.wireMethod ? [req.wireMethod] : [], + currency: Amounts.currencyOf(req.instructedAmount), + minAge: req.requiredMinimumAge ?? 0, + depositPaytoUri: req.depositPaytoUri, }); perExchange[exch.baseUrl] = { - balanceAvailable: Amounts.stringify(infoExchange.balanceAvailable), - balanceMaterial: Amounts.stringify(infoExchange.balanceMaterial), + balanceAvailable: Amounts.stringify(exchDet.balanceAvailable), + balanceMaterial: Amounts.stringify(exchDet.balanceMaterial), + balanceExchangeDepositable: Amounts.stringify( + exchDet.balanceExchangeDepositable, + ), }; } @@ -479,10 +436,13 @@ export async function reportInsufficientBalanceDetails( balanceMerchantAcceptable: Amounts.stringify( details.balanceMerchantAcceptable, ), + balanceExchangeDepositable: Amounts.stringify( + details.balanceExchangeDepositable, + ), balanceMerchantDepositable: Amounts.stringify( details.balanceMerchantDepositable, ), - maxEffectiveSpendAmount: Amounts.stringify(maxEffectiveSpendAmount), + maxEffectiveSpendAmount: Amounts.stringify(details.maxEffectiveSpendAmount), perExchange, }; } @@ -682,7 +642,7 @@ export type AvailableDenom = DenominationInfo & { numAvailable: number; }; -function findMatchingWire( +export function findMatchingWire( wireMethod: string, depositPaytoUri: string | undefined, exchangeWireDetails: ExchangeWireDetails, @@ -876,7 +836,7 @@ export type SelectPeerCoinsResult = | { type: "success"; result: PeerCoinSelectionDetails } | { type: "failure"; - insufficientBalanceDetails: PayMerchantInsufficientBalanceDetails; + insufficientBalanceDetails: PaymentInsufficientBalanceDetails; }; export interface PeerCoinSelectionRequest { @@ -1017,7 +977,6 @@ export async function selectPeerCoins( tx, selectedDenom, resCoins, - req.instructedAmount, tally, ); @@ -1046,6 +1005,7 @@ export async function selectPeerCoins( instructedAmount: req.instructedAmount, requiredMinimumAge: undefined, wireMethod: undefined, + depositPaytoUri: undefined, }, ); return { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index 240012bca..ace702e88 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -148,7 +148,7 @@ import { RemoveBackupProviderRequest, RunBackupCycleRequest, } from "./backup/index.js"; -import { MerchantPaymentBalanceDetails } from "./balance.js"; +import { PaymentBalanceDetails } from "./balance.js"; export enum WalletApiOperation { InitWallet = "initWallet", @@ -290,7 +290,7 @@ export type GetBalancesOp = { export type GetBalancesDetailOp = { op: WalletApiOperation.GetBalanceDetail; request: GetBalanceDetailRequest; - response: MerchantPaymentBalanceDetails; + response: PaymentBalanceDetails; }; export type GetPlanForOperationOp = { diff --git a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx index 731bcfed9..e7c4fbba4 100644 --- a/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx +++ b/packages/taler-wallet-webextension/src/components/PaymentButtons.tsx @@ -17,7 +17,7 @@ import { AmountJson, Amounts, - PayMerchantInsufficientBalanceDetails, + PaymentInsufficientBalanceDetails, PreparePayResult, PreparePayResultType, TranslatedString, @@ -221,7 +221,7 @@ type NoEnoughBalanceReason = | "fee-gap"; function getReason( - info: PayMerchantInsufficientBalanceDetails, + info: PaymentInsufficientBalanceDetails, ): NoEnoughBalanceReason { if (Amounts.cmp(info.amountRequested, info.balanceAvailable) > 0) { return "available"; -- cgit v1.2.3