summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-03-07 11:10:52 +0100
committerFlorian Dold <florian@dold.me>2024-03-07 11:10:52 +0100
commit466e2b7643692aa6b7f76a193b84775008e17350 (patch)
treedd3f9a0b67765e2c7ea6b97c2a7acbbcac71d4b7
parent8eb3e505be967afde0053d5a392e8c6877d8f1dd (diff)
downloadwallet-core-466e2b7643692aa6b7f76a193b84775008e17350.tar.gz
wallet-core-466e2b7643692aa6b7f76a193b84775008e17350.tar.bz2
wallet-core-466e2b7643692aa6b7f76a193b84775008e17350.zip
wallet-core: improve insufficient balance reporting
-rw-r--r--packages/taler-harness/src/harness/harness.ts62
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts166
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-util/src/errors.ts6
-rw-r--r--packages/taler-util/src/wallet-types.ts14
-rw-r--r--packages/taler-wallet-core/src/balance.ts212
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts114
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts4
-rw-r--r--packages/taler-wallet-webextension/src/components/PaymentButtons.tsx4
9 files changed, 342 insertions, 242 deletions
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<string> = 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * 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<PayMerchantInsufficientBalanceDetails> =>
- buildCodecForObject<PayMerchantInsufficientBalanceDetails>()
+ (): Codec<PaymentInsufficientBalanceDetails> =>
+ buildCodecForObject<PaymentInsufficientBalanceDetails>()
.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<MerchantPaymentBalanceDetails> {
+): Promise<PaymentBalanceDetails> {
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<MerchantPaymentBalanceDetails> {
+): Promise<PaymentBalanceDetails> {
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<MerchantPaymentBalanceDetails> {
+): Promise<PaymentBalanceDetails> {
const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
const wires = new Array<string>();
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<PeerPaymentBalanceDetails> {
- 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<PeerPaymentBalanceDetails> {
- 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<PayCoinSelection> {
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<PayMerchantInsufficientBalanceDetails> {
- const currency = Amounts.currencyOf(req.instructedAmount);
- const details = await getMerchantPaymentBalanceDetailsInTx(wex, tx, {
+): Promise<PaymentInsufficientBalanceDetails> {
+ 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";