diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/balance.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/balance.ts | 287 |
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; +} |