summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/operations/balance.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/balance.ts
parent44aaa7a636ba25b37c1c26a306e64e0db75a2747 (diff)
downloadwallet-core-92f1b5928c764b3af12a29b97bbc3e434a82b1b0.tar.gz
wallet-core-92f1b5928c764b3af12a29b97bbc3e434a82b1b0.tar.bz2
wallet-core-92f1b5928c764b3af12a29b97bbc3e434a82b1b0.zip
wallet-core: implement insufficient balance details
For now, only for merchant payments
Diffstat (limited to 'packages/taler-wallet-core/src/operations/balance.ts')
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts287
1 files changed, 254 insertions, 33 deletions
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index 95ade1cb4..f697679af 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -16,15 +16,15 @@
/**
* Functions to compute the wallet's balance.
- *
+ *
* There are multiple definition of the wallet's balance.
* We use the following terminology:
- *
+ *
* - "available": Balance that the wallet believes will certainly be available
* for spending, modulo any failures of the exchange or double spending issues.
* This includes available coins *not* allocated to any
* spending/refresh/... operation. Pending withdrawals are *not* counted
- * towards this balance, because they are not certain to succeed.
+ * towards this balance, because they are not certain to succeed.
* Pending refreshes *are* counted towards this balance.
* This balance type is nice to show to the user, because it does not
* temporarily decrease after payment when we are waiting for refreshes
@@ -38,12 +38,11 @@
*
* - "merchant-acceptable": Subset of the material balance that can be spent with a particular
* merchant (restricted via min age, exchange, auditor, wire_method).
- *
+ *
* - "merchant-depositable": Subset of the merchant-acceptable balance that the merchant
* can accept via their supported wire methods.
*/
-
/**
* Imports.
*/
@@ -52,10 +51,16 @@ import {
BalancesResponse,
Amounts,
Logger,
+ AuditorHandle,
+ ExchangeHandle,
+ canonicalizeBaseUrl,
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
-import { WalletStoresV1 } from "../db.js";
+import { AllowedAuditorInfo, AllowedExchangeInfo, RefreshGroupRecord, WalletStoresV1 } from "../db.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { getExchangeDetails } from "./exchanges.js";
+import { checkLogicInvariant } from "../util/invariants.js";
/**
* Logger.
@@ -69,6 +74,30 @@ interface WalletBalance {
}
/**
+ * Compute the available amount that the wallet expects to get
+ * out of a refresh group.
+ */
+function computeRefreshGroupAvailableAmount(r: RefreshGroupRecord): AmountJson {
+ // Don't count finished refreshes, since the refresh already resulted
+ // in coins being added to the wallet.
+ let available = Amounts.zeroOfCurrency(r.currency);
+ if (r.timestampFinished) {
+ return available;
+ }
+ for (let i = 0; i < r.oldCoinPubs.length; i++) {
+ const session = r.refreshSessionPerCoin[i];
+ if (session) {
+ // We are always assuming the refresh will succeed, thus we
+ // report the output as available balance.
+ available = Amounts.add(available, session.amountRefreshOutput).amount;
+ } else {
+ available = Amounts.add(available, r.estimatedOutputPerCoin[i]).amount;
+ }
+ }
+ return available;
+}
+
+/**
* Get balance information.
*/
export async function getBalancesInsideTransaction(
@@ -110,33 +139,11 @@ export async function getBalancesInsideTransaction(
});
await tx.refreshGroups.iter().forEach((r) => {
- // Don't count finished refreshes, since the refresh already resulted
- // in coins being added to the wallet.
- if (r.timestampFinished) {
- return;
- }
- for (let i = 0; i < r.oldCoinPubs.length; i++) {
- const session = r.refreshSessionPerCoin[i];
- if (session) {
- const currency = Amounts.parseOrThrow(
- session.amountRefreshOutput,
- ).currency;
- const b = initBalance(currency);
- // We are always assuming the refresh will succeed, thus we
- // report the output as available balance.
- b.available = Amounts.add(
- b.available,
- session.amountRefreshOutput,
- ).amount;
- } else {
- const currency = Amounts.parseOrThrow(r.inputPerCoin[i]).currency;
- const b = initBalance(currency);
- b.available = Amounts.add(
- b.available,
- r.estimatedOutputPerCoin[i],
- ).amount;
- }
- }
+ const b = initBalance(r.currency);
+ b.available = Amounts.add(
+ b.available,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
});
await tx.withdrawalGroups.iter().forEach((wds) => {
@@ -194,3 +201,217 @@ 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 MerchantPaymentRestrictionsForBalance {
+ currency: string;
+ minAge: number;
+ acceptedExchanges: AllowedExchangeInfo[];
+ acceptedAuditors: AllowedAuditorInfo[];
+ acceptedWireMethods: string[];
+}
+
+export interface AcceptableExchanges {
+ /**
+ * Exchanges accepted by the merchant, but wire method might not match.
+ */
+ acceptableExchanges: string[];
+
+ /**
+ * Exchanges accepted by the merchant, including a matching
+ * wire method, i.e. the merchant can deposit coins there.
+ */
+ depositableExchanges: string[];
+}
+
+/**
+ * Get all exchanges that are acceptable for a particular payment.
+ */
+export async function getAcceptableExchangeBaseUrls(
+ ws: InternalWalletState,
+ req: MerchantPaymentRestrictionsForBalance,
+): Promise<AcceptableExchanges> {
+ const acceptableExchangeUrls = new Set<string>();
+ const depositableExchangeUrls = new Set<string>();
+ await ws.db
+ .mktx((x) => [x.exchanges, x.exchangeDetails, x.auditorTrust])
+ .runReadOnly(async (tx) => {
+ // FIXME: We should have a DB index to look up all exchanges
+ // for a particular auditor ...
+
+ const canonExchanges = new Set<string>();
+ const canonAuditors = new Set<string>();
+
+ for (const exchangeHandle of req.acceptedExchanges) {
+ const normUrl = canonicalizeBaseUrl(exchangeHandle.exchangeBaseUrl);
+ canonExchanges.add(normUrl);
+ }
+
+ for (const auditorHandle of req.acceptedAuditors) {
+ const normUrl = canonicalizeBaseUrl(auditorHandle.auditorBaseUrl);
+ canonAuditors.add(normUrl);
+ }
+
+ await tx.exchanges.iter().forEachAsync(async (exchange) => {
+ const dp = exchange.detailsPointer;
+ if (!dp) {
+ return;
+ }
+ const { currency, masterPublicKey } = dp;
+ const exchangeDetails = await tx.exchangeDetails.indexes.byPointer.get([
+ exchange.baseUrl,
+ currency,
+ masterPublicKey,
+ ]);
+ if (!exchangeDetails) {
+ return;
+ }
+
+ let acceptable = false;
+
+ if (canonExchanges.has(exchange.baseUrl)) {
+ acceptableExchangeUrls.add(exchange.baseUrl);
+ acceptable = true;
+ }
+ for (const exchangeAuditor of exchangeDetails.auditors) {
+ if (canonAuditors.has(exchangeAuditor.auditor_url)) {
+ acceptableExchangeUrls.add(exchange.baseUrl);
+ acceptable = true;
+ break;
+ }
+ }
+
+ if (!acceptable) {
+ return;
+ }
+ // FIXME: Also consider exchange and auditor public key
+ // instead of just base URLs?
+
+ let wireMethodSupported = false;
+ for (const acc of exchangeDetails.wireInfo.accounts) {
+ const pp = parsePaytoUri(acc.payto_uri);
+ checkLogicInvariant(!!pp);
+ for (const wm of req.acceptedWireMethods) {
+ if (pp.targetType === wm) {
+ wireMethodSupported = true;
+ break;
+ }
+ if (wireMethodSupported) {
+ break;
+ }
+ }
+ }
+
+ acceptableExchangeUrls.add(exchange.baseUrl);
+ if (wireMethodSupported) {
+ depositableExchangeUrls.add(exchange.baseUrl);
+ }
+ });
+ });
+ return {
+ acceptableExchanges: [...acceptableExchangeUrls],
+ depositableExchanges: [...depositableExchangeUrls],
+ };
+}
+
+export interface MerchantPaymentBalanceDetails {
+ /**
+ * Balance of type "available" (see balance.ts for definition).
+ */
+ balanceAvailable: AmountJson;
+
+ /**
+ * Balance of type "material" (see balance.ts for definition).
+ */
+ balanceMaterial: AmountJson;
+
+ /**
+ * Balance of type "age-acceptable" (see balance.ts for definition).
+ */
+ balanceAgeAcceptable: AmountJson;
+
+ /**
+ * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ */
+ balanceMerchantAcceptable: AmountJson;
+
+ /**
+ * Balance of type "merchant-depositable" (see balance.ts for definition).
+ */
+ balanceMerchantDepositable: AmountJson;
+}
+
+export async function getMerchantPaymentBalanceDetails(
+ ws: InternalWalletState,
+ req: MerchantPaymentRestrictionsForBalance,
+): Promise<MerchantPaymentBalanceDetails> {
+ const acceptability = await getAcceptableExchangeBaseUrls(ws, req);
+
+ const d: MerchantPaymentBalanceDetails = {
+ 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),
+ };
+
+ const wbal = await ws.db
+ .mktx((x) => [
+ x.coins,
+ x.coinAvailability,
+ x.refreshGroups,
+ x.purchases,
+ x.withdrawalGroups,
+ ])
+ .runReadOnly(async (tx) => {
+ await tx.coinAvailability.iter().forEach((ca) => {
+ const singleCoinAmount: AmountJson = {
+ currency: ca.currency,
+ fraction: ca.amountFrac,
+ value: ca.amountVal,
+ };
+ const coinAmount: AmountJson = Amounts.mult(
+ singleCoinAmount,
+ ca.freshCoinCount,
+ ).amount;
+ d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
+ d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
+ if (ca.maxAge === 0 || ca.maxAge > req.minAge) {
+ d.balanceAgeAcceptable = Amounts.add(
+ d.balanceAgeAcceptable,
+ coinAmount,
+ ).amount;
+ if (acceptability.acceptableExchanges.includes(ca.exchangeBaseUrl)) {
+ d.balanceMerchantAcceptable = Amounts.add(
+ d.balanceMerchantAcceptable,
+ coinAmount,
+ ).amount;
+ if (
+ acceptability.depositableExchanges.includes(ca.exchangeBaseUrl)
+ ) {
+ d.balanceMerchantDepositable = Amounts.add(
+ d.balanceMerchantDepositable,
+ coinAmount,
+ ).amount;
+ }
+ }
+ }
+ });
+
+ await tx.refreshGroups.iter().forEach((r) => {
+ d.balanceAvailable = Amounts.add(
+ d.balanceAvailable,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
+ });
+ });
+
+ return d;
+}