/* This file is part of GNU Taler (C) 2019 GNUnet e.V. 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 */ /** * 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. * 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 * * - "material": Balance that the wallet believes it could spend *right now*, * without waiting for any operations to complete. * This balance type is important when showing "insufficient balance" error messages. * * - "age-acceptable": Subset of the material balance that can be spent * with age restrictions applied. * * - "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. */ import { AmountJson, BalancesResponse, Amounts, Logger, } from "@gnu-taler/taler-util"; import { WalletStoresV1 } from "../db.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { InternalWalletState } from "../internal-wallet-state.js"; /** * Logger. */ const logger = new Logger("operations/balance.ts"); interface WalletBalance { available: AmountJson; pendingIncoming: AmountJson; pendingOutgoing: AmountJson; } /** * Get balance information. */ export async function getBalancesInsideTransaction( ws: InternalWalletState, tx: GetReadOnlyAccess<{ coins: typeof WalletStoresV1.coins; coinAvailability: typeof WalletStoresV1.coinAvailability; refreshGroups: typeof WalletStoresV1.refreshGroups; withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; }>, ): Promise { const balanceStore: Record = {}; /** * Add amount to a balance field, both for * the slicing by exchange and currency. */ const initBalance = (currency: string): WalletBalance => { const b = balanceStore[currency]; if (!b) { balanceStore[currency] = { available: Amounts.zeroOfCurrency(currency), pendingIncoming: Amounts.zeroOfCurrency(currency), pendingOutgoing: Amounts.zeroOfCurrency(currency), }; } return balanceStore[currency]; }; await tx.coinAvailability.iter().forEach((ca) => { const b = initBalance(ca.currency); for (let i = 0; i < ca.freshCoinCount; i++) { b.available = Amounts.add(b.available, { currency: ca.currency, fraction: ca.amountFrac, value: ca.amountVal, }).amount; } }); 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; } } }); await tx.withdrawalGroups.iter().forEach((wds) => { if (wds.timestampFinish) { return; } const b = initBalance(Amounts.currencyOf(wds.denomsSel.totalWithdrawCost)); b.pendingIncoming = Amounts.add( b.pendingIncoming, wds.denomsSel.totalCoinValue, ).amount; }); const balancesResponse: BalancesResponse = { balances: [], }; Object.keys(balanceStore) .sort() .forEach((c) => { const v = balanceStore[c]; balancesResponse.balances.push({ available: Amounts.stringify(v.available), pendingIncoming: Amounts.stringify(v.pendingIncoming), pendingOutgoing: Amounts.stringify(v.pendingOutgoing), hasPendingTransactions: false, requiresUserInput: false, }); }); return balancesResponse; } /** * Get detailed balance information, sliced by exchange and by currency. */ export async function getBalances( ws: InternalWalletState, ): Promise { logger.trace("starting to compute balance"); const wbal = await ws.db .mktx((x) => [ x.coins, x.coinAvailability, x.refreshGroups, x.purchases, x.withdrawalGroups, ]) .runReadOnly(async (tx) => { return getBalancesInsideTransaction(ws, tx); }); logger.trace("finished computing wallet balance"); return wbal; }