summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/balance.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/balance.ts')
-rw-r--r--packages/taler-wallet-core/src/balance.ts772
1 files changed, 772 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..5a805b477
--- /dev/null
+++ b/packages/taler-wallet-core/src/balance.ts
@@ -0,0 +1,772 @@
+/*
+ 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 {
+ AmountJson,
+ AmountLike,
+ Amounts,
+ assertUnreachable,
+ BalanceFlag,
+ BalancesResponse,
+ GetBalanceDetailRequest,
+ j2s,
+ Logger,
+ parsePaytoUri,
+ ScopeInfo,
+ ScopeType,
+} from "@gnu-taler/taler-util";
+import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js";
+import {
+ DepositOperationStatus,
+ ExchangeEntryDbRecordStatus,
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ PeerPushDebitStatus,
+ RefreshGroupRecord,
+ RefreshOperationStatus,
+ WalletDbReadOnlyTransaction,
+ WithdrawalGroupStatus,
+} from "./db.js";
+import {
+ getExchangeScopeInfo,
+ getExchangeWireDetailsInTx,
+} from "./exchanges.js";
+import { getDenomInfo, WalletExecutionContext } from "./wallet.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 wex: WalletExecutionContext,
+ 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 addZero(currency: string, exchangeBaseUrl: string): Promise<void> {
+ await this.initBalance(currency, exchangeBaseUrl);
+ }
+
+ 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.pendingIncoming, amount).amount;
+ }
+
+ async addPendingOutgoing(
+ currency: string,
+ exchangeBaseUrl: string,
+ amount: AmountLike,
+ ): Promise<void> {
+ const b = await this.initBalance(currency, exchangeBaseUrl);
+ b.pendingOutgoing = Amounts.add(b.pendingOutgoing, 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(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "exchanges",
+ "exchangeDetails",
+ "coinAvailability",
+ "refreshGroups",
+ "depositGroups",
+ "withdrawalGroups",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "peerPushDebit",
+ ]
+ >,
+): Promise<BalancesResponse> {
+ const balanceStore: BalancesStore = new BalancesStore(wex, tx);
+
+ const keyRangeActive = GlobalIDB.KeyRange.bound(
+ OPERATION_STATUS_ACTIVE_FIRST,
+ OPERATION_STATUS_ACTIVE_LAST,
+ );
+
+ await tx.exchanges.iter().forEachAsync(async (ex) => {
+ if (
+ ex.entryStatus === ExchangeEntryDbRecordStatus.Used ||
+ ex.tosAcceptedTimestamp != null
+ ) {
+ const det = await getExchangeWireDetailsInTx(tx, ex.baseUrl);
+ if (det) {
+ await balanceStore.addZero(det.currency, ex.baseUrl);
+ }
+ }
+ });
+
+ await tx.coinAvailability.iter().forEachAsync(async (ca) => {
+ const count = ca.visibleCoinCount ?? 0;
+ await balanceStore.addZero(ca.currency, ca.exchangeBaseUrl);
+ 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.AbortedOtherWallet:
+ case WithdrawalGroupStatus.AbortedUserRefused:
+ case WithdrawalGroupStatus.DialogProposed:
+ 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.peerPushDebit.indexes.byStatus
+ .iter(keyRangeActive)
+ .forEachAsync(async (ppdRecord) => {
+ switch (ppdRecord.status) {
+ case PeerPushDebitStatus.AbortingDeletePurse:
+ case PeerPushDebitStatus.SuspendedAbortingDeletePurse:
+ case PeerPushDebitStatus.PendingReady:
+ case PeerPushDebitStatus.SuspendedReady:
+ case PeerPushDebitStatus.PendingCreatePurse:
+ case PeerPushDebitStatus.SuspendedCreatePurse: {
+ const currency = Amounts.currencyOf(ppdRecord.amount);
+ await balanceStore.addPendingOutgoing(
+ currency,
+ ppdRecord.exchangeBaseUrl,
+ ppdRecord.totalCost,
+ );
+ break;
+ }
+ }
+ });
+
+ 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);
+ }
+
+ switch (dgRecord.operationStatus) {
+ case DepositOperationStatus.SuspendedKyc:
+ case DepositOperationStatus.PendingKyc:
+ case DepositOperationStatus.PendingTrack:
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.SuspendedDeposit:
+ case DepositOperationStatus.SuspendedTrack:
+ case DepositOperationStatus.PendingDeposit: {
+ const perExchange = dgRecord.infoPerExchange;
+ if (perExchange) {
+ for (const [e, v] of Object.entries(perExchange)) {
+ await balanceStore.addPendingOutgoing(
+ currency,
+ e,
+ v.amountEffective,
+ );
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return balanceStore.toBalancesResponse();
+}
+
+/**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+export async function getBalances(
+ wex: WalletExecutionContext,
+): Promise<BalancesResponse> {
+ logger.trace("starting to compute balance");
+
+ const wbal = await wex.db.runReadWriteTx(
+ {
+ storeNames: [
+ "coinAvailability",
+ "coins",
+ "depositGroups",
+ "exchangeDetails",
+ "exchanges",
+ "globalCurrencyAuditors",
+ "globalCurrencyExchanges",
+ "purchases",
+ "refreshGroups",
+ "withdrawalGroups",
+ "peerPushDebit",
+ ],
+ },
+ async (tx) => {
+ return getBalancesInsideTransaction(wex, tx);
+ },
+ );
+
+ logger.trace("finished computing wallet balance");
+
+ return wbal;
+}
+
+export interface PaymentRestrictionsForBalance {
+ currency: string;
+ minAge: number;
+ restrictExchanges: ExchangeRestrictionSpec | undefined;
+ restrictWireMethods: string[] | undefined;
+ depositPaytoUri: string | undefined;
+}
+
+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[];
+}
+
+export interface PaymentBalanceDetails {
+ /**
+ * 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).
+ */
+ balanceReceiverAcceptable: AmountJson;
+
+ /**
+ * Balance of type "merchant-depositable" (see balance.ts for definition).
+ */
+ balanceReceiverDepositable: 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 getPaymentBalanceDetails(
+ wex: WalletExecutionContext,
+ req: PaymentRestrictionsForBalance,
+): Promise<PaymentBalanceDetails> {
+ return await wex.db.runReadOnlyTx(
+ {
+ storeNames: [
+ "coinAvailability",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ ],
+ },
+ async (tx) => {
+ return getPaymentBalanceDetailsInTx(wex, tx, req);
+ },
+ );
+}
+
+export async function getPaymentBalanceDetailsInTx(
+ wex: WalletExecutionContext,
+ tx: WalletDbReadOnlyTransaction<
+ [
+ "coinAvailability",
+ "refreshGroups",
+ "exchanges",
+ "exchangeDetails",
+ "denominations",
+ ]
+ >,
+ req: PaymentRestrictionsForBalance,
+): Promise<PaymentBalanceDetails> {
+ const d: PaymentBalanceDetails = {
+ balanceAvailable: Amounts.zeroOfCurrency(req.currency),
+ balanceMaterial: Amounts.zeroOfCurrency(req.currency),
+ balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverDepositable: Amounts.zeroOfCurrency(req.currency),
+ maxEffectiveSpendAmount: Amounts.zeroOfCurrency(req.currency),
+ balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency),
+ };
+
+ logger.info(`computing balance details for ${j2s(req)}`);
+
+ const availableCoins = await tx.coinAvailability.getAll();
+
+ for (const ca of availableCoins) {
+ if (ca.currency != req.currency) {
+ 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;
+
+ let wireOkay = false;
+ if (req.restrictWireMethods == null) {
+ wireOkay = true;
+ } else {
+ for (const wm of req.restrictWireMethods) {
+ const wmf = findMatchingWire(wm, req.depositPaytoUri, wireDetails);
+ if (wmf) {
+ wireOkay = true;
+ break;
+ }
+ }
+ }
+
+ if (wireOkay) {
+ d.balanceExchangeDepositable = Amounts.add(
+ d.balanceExchangeDepositable,
+ coinAmount,
+ ).amount;
+ }
+
+ let ageOkay = ca.maxAge === 0 || ca.maxAge > req.minAge;
+
+ let merchantExchangeAcceptable = false;
+
+ if (!req.restrictExchanges) {
+ merchantExchangeAcceptable = true;
+ } else {
+ for (const ex of req.restrictExchanges.exchanges) {
+ if (ex.exchangeBaseUrl === ca.exchangeBaseUrl) {
+ merchantExchangeAcceptable = true;
+ break;
+ }
+ }
+ for (const acceptedAuditor of req.restrictExchanges.auditors) {
+ for (const exchangeAuditor of wireDetails.auditors) {
+ if (acceptedAuditor.auditorBaseUrl === exchangeAuditor.auditor_url) {
+ merchantExchangeAcceptable = true;
+ break;
+ }
+ }
+ }
+ }
+
+ const merchantExchangeDepositable = merchantExchangeAcceptable && wireOkay;
+
+ d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
+ d.balanceMaterial = Amounts.add(d.balanceMaterial, coinAmount).amount;
+ if (ageOkay) {
+ d.balanceAgeAcceptable = Amounts.add(
+ d.balanceAgeAcceptable,
+ coinAmount,
+ ).amount;
+ if (merchantExchangeAcceptable) {
+ d.balanceReceiverAcceptable = Amounts.add(
+ d.balanceReceiverAcceptable,
+ coinAmount,
+ ).amount;
+ if (merchantExchangeDepositable) {
+ d.balanceReceiverDepositable = Amounts.add(
+ d.balanceReceiverDepositable,
+ coinAmount,
+ ).amount;
+ }
+ }
+ }
+
+ if (
+ ageOkay &&
+ wireOkay &&
+ merchantExchangeAcceptable &&
+ merchantExchangeDepositable
+ ) {
+ 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;
+ }
+ }
+
+ 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(
+ wex: WalletExecutionContext,
+ req: GetBalanceDetailRequest,
+): Promise<PaymentBalanceDetails> {
+ const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
+ const wires = new Array<string>();
+ await wex.db.runReadOnlyTx(
+ { storeNames: ["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 getPaymentBalanceDetails(wex, {
+ currency: req.currency,
+ restrictExchanges: {
+ auditors: [],
+ exchanges,
+ },
+ restrictWireMethods: wires,
+ minAge: 0,
+ depositPaytoUri: undefined,
+ });
+}