summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/balance.ts
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-02-19 18:05:48 +0100
committerFlorian Dold <florian@dold.me>2024-02-19 18:05:48 +0100
commite951075d2ef52fa8e9e7489c62031777c3a7e66b (patch)
tree64208c09a9162f3a99adccf30edc36de1ef884ef /packages/taler-wallet-core/src/balance.ts
parente975740ac4e9ba4bc531226784d640a018c00833 (diff)
downloadwallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.gz
wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.tar.bz2
wallet-core-e951075d2ef52fa8e9e7489c62031777c3a7e66b.zip
wallet-core: flatten directory structure
Diffstat (limited to 'packages/taler-wallet-core/src/balance.ts')
-rw-r--r--packages/taler-wallet-core/src/balance.ts730
1 files changed, 730 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
new file mode 100644
index 000000000..700e2c5d7
--- /dev/null
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -0,0 +1,730 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Functions to compute the wallet's balance.
+ *
+ * There are multiple definition of the wallet's balance.
+ * We use the following terminology:
+ *
+ * - "available": Balance that is available
+ * for spending from transactions in their final state and
+ * expected to be available from pending refreshes.
+ *
+ * - "pending-incoming": Expected (positive!) delta
+ * to the available balance that we expect to have
+ * after pending operations reach the "done" state.
+ *
+ * - "pending-outgoing": Amount that is currently allocated
+ * to be spent, but the spend operation could still be aborted
+ * and part of the pending-outgoing amount could be recovered.
+ *
+ * - "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 { GlobalIDB } from "@gnu-taler/idb-bridge";
+import {
+ AllowedAuditorInfo,
+ AllowedExchangeInfo,
+ AmountJson,
+ AmountLike,
+ Amounts,
+ BalanceFlag,
+ BalancesResponse,
+ canonicalizeBaseUrl,
+ GetBalanceDetailRequest,
+ Logger,
+ parsePaytoUri,
+ ScopeInfo,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import {
+ DepositOperationStatus,
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ WalletDbReadOnlyTransaction,
+ WithdrawalGroupStatus,
+} from "./db.js";
+import { InternalWalletState } from "./internal-wallet-state.js";
+import { assertUnreachable } from "./util/assertUnreachable.js";
+import { checkLogicInvariant } from "./util/invariants.js";
+import {
+ getExchangeScopeInfo,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
+
+/**
+ * Logger.
+ */
+const logger = new Logger("operations/balance.ts");
+
+interface WalletBalance {
+ scopeInfo: ScopeInfo;
+ available: AmountJson;
+ pendingIncoming: AmountJson;
+ pendingOutgoing: AmountJson;
+ flagIncomingKyc: boolean;
+ flagIncomingAml: boolean;
+ flagIncomingConfirmation: boolean;
+ flagOutgoingKyc: boolean;
+}
+
+/**
+ * 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++) {
+ available = Amounts.add(available, r.expectedOutputPerCoin[i]).amount;
+ }
+ return available;
+}
+
+function getBalanceKey(scopeInfo: ScopeInfo): string {
+ switch (scopeInfo.type) {
+ case ScopeType.Auditor:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Exchange:
+ return `${scopeInfo.type};${scopeInfo.currency};${scopeInfo.url}`;
+ case ScopeType.Global:
+ return `${scopeInfo.type};${scopeInfo.currency}`;
+ }
+}
+
+class BalancesStore {
+ private exchangeScopeCache: Record<string, ScopeInfo> = {};
+ private balanceStore: Record<string, WalletBalance> = {};
+
+ constructor(
+ private ws: InternalWalletState,
+ private tx: WalletDbReadOnlyTransaction<
+ [
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "exchanges",
+ "exchangeDetails",
+ ]
+ >,
+ ) {}
+
+ /**
+ * Add amount to a balance field, both for
+ * the slicing by exchange and currency.
+ */
+ private async initBalance(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<WalletBalance> {
+ let scopeInfo: ScopeInfo | undefined =
+ this.exchangeScopeCache[exchangeBaseUrl];
+ if (!scopeInfo) {
+ scopeInfo = await getExchangeScopeInfo(
+ this.tx,
+ exchangeBaseUrl,
+ currency,
+ );
+ this.exchangeScopeCache[exchangeBaseUrl] = scopeInfo;
+ }
+ const balanceKey = getBalanceKey(scopeInfo);
+ const b = this.balanceStore[balanceKey];
+ if (!b) {
+ const zero = Amounts.zeroOfCurrency(currency);
+ this.balanceStore[balanceKey] = {
+ scopeInfo,
+ available: zero,
+ pendingIncoming: zero,
+ pendingOutgoing: zero,
+ flagIncomingAml: false,
+ flagIncomingConfirmation: false,
+ flagIncomingKyc: false,
+ flagOutgoingKyc: false,
+ };
+ }
+ return this.balanceStore[balanceKey];
+ }
+
+ async addAvailable(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.available = Amounts.add(b.available, amount).amount;
+ }
+
+ async addPendingIncoming(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.pendingIncoming = Amounts.add(b.available, amount).amount;
+ }
+
+ async setFlagIncomingAml(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingAml = true;
+ }
+
+ async setFlagIncomingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingKyc = true;
+ }
+
+ async setFlagIncomingConfirmation(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagIncomingConfirmation = true;
+ }
+
+ async setFlagOutgoingKyc(
+ currency: string,
+ exchangeBaseUrl: string,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.flagOutgoingKyc = true;
+ }
+
+ toBalancesResponse(): BalancesResponse {
+ const balancesResponse: BalancesResponse = {
+ balances: [],
+ };
+
+ const balanceStore = this.balanceStore;
+
+ Object.keys(balanceStore)
+ .sort()
+ .forEach((c) => {
+ const v = balanceStore[c];
+ const flags: BalanceFlag[] = [];
+ if (v.flagIncomingAml) {
+ flags.push(BalanceFlag.IncomingAml);
+ }
+ if (v.flagIncomingKyc) {
+ flags.push(BalanceFlag.IncomingKyc);
+ }
+ if (v.flagIncomingConfirmation) {
+ flags.push(BalanceFlag.IncomingConfirmation);
+ }
+ if (v.flagOutgoingKyc) {
+ flags.push(BalanceFlag.OutgoingKyc);
+ }
+ balancesResponse.balances.push({
+ scopeInfo: v.scopeInfo,
+ available: Amounts.stringify(v.available),
+ pendingIncoming: Amounts.stringify(v.pendingIncoming),
+ pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
+ // FIXME: This field is basically not implemented, do we even need it?
+ hasPendingTransactions: false,
+ // FIXME: This field is basically not implemented, do we even need it?
+ requiresUserInput: false,
+ flags,
+ });
+ });
+ return balancesResponse;
+ }
+}
+
+/**
+ * Get balance information.
+ */
+export async function getBalancesInsideTransaction(
+ ws: InternalWalletState,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "coinAvailability",
+ "refreshGroups",
+ "depositGroups",
+ "withdrawalGroups",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ ]
+ >,
+): Promise<BalancesResponse> {
+ const balanceStore: BalancesStore = new BalancesStore(ws, tx);
+
+ const keyRangeActive = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+
+ await tx.coinAvailability.iter().forEachAsync(async (ca) => {
+ const count = ca.visibleCoinCount ?? 0;
+ for (let i = 0; i < count; i++) {
+ await balanceStore.addAvailable(
+ ca.currency,
+ ca.exchangeBaseUrl,
+ ca.value,
+ );
+ }
+ });
+
+ await tx.refreshGroups.iter().forEachAsync(async (r) => {
+ switch (r.operationStatus) {
+ case RefreshOperationStatus.Pending:
+ case RefreshOperationStatus.Suspended:
+ break;
+ default:
+ return;
+ }
+ const perExchange = r.infoPerExchange;
+ if (!perExchange) {
+ return;
+ }
+ for (const [e, x] of Object.entries(perExchange)) {
+ await balanceStore.addAvailable(r.currency, e, x.outputEffective);
+ }
+ });
+
+ await tx.withdrawalGroups.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (wgRecord) => {
+ const currency = Amounts.currencyOf(wgRecord.denomsSel.totalCoinValue);
+ switch (wgRecord.status) {
+ case WithdrawalGroupStatus.AbortedBank:
+ case WithdrawalGroupStatus.AbortedExchange:
+ case WithdrawalGroupStatus.FailedAbortingBank:
+ case WithdrawalGroupStatus.FailedBankAborted:
+ case WithdrawalGroupStatus.Done:
+ // Does not count as pendingIncoming
+ return;
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.AbortingBank:
+ case WithdrawalGroupStatus.PendingQueryingStatus:
+ case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
+ case WithdrawalGroupStatus.SuspendedReady:
+ case WithdrawalGroupStatus.SuspendedRegisteringBank:
+ case WithdrawalGroupStatus.SuspendedAbortingBank:
+ case WithdrawalGroupStatus.SuspendedQueryingStatus:
+ // Pending, but no special flag.
+ break;
+ case WithdrawalGroupStatus.SuspendedKyc:
+ case WithdrawalGroupStatus.PendingKyc:
+ await balanceStore.setFlagIncomingKyc(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ case WithdrawalGroupStatus.PendingAml:
+ case WithdrawalGroupStatus.SuspendedAml:
+ await balanceStore.setFlagIncomingAml(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank:
+ await balanceStore.setFlagIncomingConfirmation(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ );
+ break;
+ default:
+ assertUnreachable(wgRecord.status);
+ }
+ await balanceStore.addPendingIncoming(
+ currency,
+ wgRecord.exchangeBaseUrl,
+ wgRecord.denomsSel.totalCoinValue,
+ );
+ });
+
+ await tx.depositGroups.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (dgRecord) => {
+ const perExchange = dgRecord.infoPerExchange;
+ if (!perExchange) {
+ return;
+ }
+ for (const [e, x] of Object.entries(perExchange)) {
+ const currency = Amounts.currencyOf(dgRecord.amount);
+ switch (dgRecord.operationStatus) {
+ case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.PendingKyc:
+ await balanceStore.setFlagOutgoingKyc(currency, e);
+ }
+ }
+ });
+
+ return balanceStore.toBalancesResponse();
+}
+
+/**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+export async function getBalances(
+ ws: InternalWalletState,
+): Promise<BalancesResponse> {
+ logger.trace("starting to compute balance");
+
+ const wbal = await ws.db.runReadWriteTx(
+ [
+ "coinAvailability",
+ "coins",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "purchases",
+ "refreshGroups",
+ "withdrawalGroups",
+ ],
+ async (tx) => {
+ return getBalancesInsideTransaction(ws, tx);
+ },
+ );
+
+ logger.trace("finished computing wallet balance");
+
+ 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.runReadOnlyTx(["exchanges", "exchangeDetails"], 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),
+ };
+
+ await ws.db.runReadOnlyTx(
+ ["coinAvailability", "refreshGroups"],
+ async (tx) => {
+ await tx.coinAvailability.iter().forEach((ca) => {
+ if (ca.currency != req.currency) {
+ return;
+ }
+ const singleCoinAmount: AmountJson = Amounts.parseOrThrow(ca.value);
+ 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) => {
+ if (r.currency != req.currency) {
+ return;
+ }
+ d.balanceAvailable = Amounts.add(
+ d.balanceAvailable,
+ computeRefreshGroupAvailableAmount(r),
+ ).amount;
+ });
+ },
+ );
+
+ return d;
+}
+
+export async function getBalanceDetail(
+ ws: InternalWalletState,
+ req: GetBalanceDetailRequest,
+): Promise<MerchantPaymentBalanceDetails> {
+ const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
+ const wires = new Array<string>();
+ await ws.db.runReadOnlyTx(["exchanges", "exchangeDetails"], async (tx) => {
+ const allExchanges = await tx.exchanges.iter().toArray();
+ for (const e of allExchanges) {
+ const details = await getExchangeWireDetailsInTx(tx, e.baseUrl);
+ if (!details || req.currency !== details.currency) {
+ continue;
+ }
+ details.wireInfo.accounts.forEach((a) => {
+ const payto = parsePaytoUri(a.payto_uri);
+ if (payto && !wires.includes(payto.targetType)) {
+ wires.push(payto.targetType);
+ }
+ });
+ exchanges.push({
+ exchangePub: details.masterPublicKey,
+ exchangeBaseUrl: e.baseUrl,
+ });
+ }
+ });
+
+ return await getMerchantPaymentBalanceDetails(ws, {
+ currency: req.currency,
+ acceptedAuditors: [],
+ acceptedExchanges: exchanges,
+ acceptedWireMethods: wires,
+ minAge: 0,
+ });
+}
+
+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(
+ ws: InternalWalletState,
+ 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,
+ };
+}