taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit a8e9305b055010fef7dfeb746d081ca4eab81a82
parent c778af04f43871530c1cac29b281950f1a87dff7
Author: Florian Dold <dold@taler.net>
Date:   Sat, 18 Jul 2026 00:29:31 +0200

wallet-core: migrate denominations.ts and balance.ts to abstract database access layer

Diffstat:
Mpackages/taler-wallet-core/src/balance.ts | 592++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/taler-wallet-core/src/coinSelection.ts | 3++-
Mpackages/taler-wallet-core/src/db-common.ts | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/db-indexeddb.ts | 136++-----------------------------------------------------------------------------
Mpackages/taler-wallet-core/src/dbtx.ts | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/denominations.ts | 11++++++-----
Mpackages/taler-wallet-core/src/exchanges.ts | 4++--
Mpackages/taler-wallet-core/src/wallet.ts | 7+++++--
8 files changed, 698 insertions(+), 448 deletions(-)

diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts @@ -59,7 +59,7 @@ /** * Imports. */ -import { GlobalIDB } from "@gnu-taler/idb-bridge"; + import { AllowedExchangeInfo, AmountJson, @@ -82,8 +82,6 @@ import { ExchangeRestrictionSpec, findMatchingWire } from "./coinSelection.js"; import { DepositOperationStatus, ExchangeEntryDbRecordStatus, - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, PeerPullDebitRecordStatus, PeerPullPaymentCreditStatus, PeerPushCreditStatus, @@ -93,20 +91,16 @@ import { WithdrawalGroupStatus, } from "./db-common.js"; import { + WithdrawalRecordType, DonationSummaryRecord, RefreshGroupRecord, - WithdrawalRecordType, } from "./db-indexeddb.js"; import { - checkExchangeInScopeTx, - getExchangeDetailsInTx, - getExchangeScopeInfo, -} from "./exchanges.js"; -import { getDenomInfo, - LegacyWalletTxHandle, WalletExecutionContext, + LegacyWalletTxHandle, } from "./wallet.js"; +import { WalletDbTransaction } from "./dbtx.js"; /** * Logger. @@ -184,7 +178,7 @@ class BalancesStore { constructor( private wex: WalletExecutionContext, - private tx: LegacyWalletTxHandle, + private tx: WalletDbTransaction, ) {} setNonDemoExchange(baseUrl: string) { @@ -202,11 +196,7 @@ class BalancesStore { let scopeInfo: ScopeInfo | undefined = this.exchangeScopeCache[exchangeBaseUrl]; if (!scopeInfo) { - scopeInfo = await getExchangeScopeInfo( - this.tx, - exchangeBaseUrl, - currency, - ); + scopeInfo = await this.tx.getExchangeScopeInfo(exchangeBaseUrl, currency); this.exchangeScopeCache[exchangeBaseUrl] = scopeInfo; } const balanceKey = getBalanceKey(scopeInfo); @@ -401,25 +391,22 @@ class BalancesStore { */ export async function getBalancesInsideTransaction( wex: WalletExecutionContext, - tx: LegacyWalletTxHandle, + tx: WalletDbTransaction, ): Promise<BalancesResponse> { const balanceStore: BalancesStore = new BalancesStore(wex, tx); - const keyRangeActive = GlobalIDB.KeyRange.bound( - OPERATION_STATUS_NONFINAL_FIRST, - OPERATION_STATUS_NONFINAL_LAST, - ); - - await tx.donationSummaries.iter().forEachAsync(async (rec) => { + const donationSummaries = await tx.getDonationSummaries(); + for (const rec of donationSummaries) { balanceStore.addDonauItem(rec); - }); + } - await tx.exchanges.iter().forEachAsync(async (ex) => { + const exchanges = await tx.getExchanges(); + for (const ex of exchanges) { if ( ex.entryStatus === ExchangeEntryDbRecordStatus.Used || ex.tosAcceptedTimestamp != null ) { - const det = await getExchangeDetailsInTx(tx, ex.baseUrl); + const det = await tx.getExchangeDetails(ex.baseUrl); if (det) { if (ex.presetType == null || ex.presetType === "prod") { balanceStore.setNonDemoExchange(ex.baseUrl); @@ -443,9 +430,10 @@ export async function getBalancesInsideTransaction( } } } - }); + } - await tx.coinAvailability.iter().forEachAsync(async (ca) => { + const coinAvailability = await tx.getCoinAvailabilities(); + for (const ca of coinAvailability) { const count = ca.visibleCoinCount ?? 0; await balanceStore.addZero(ca.currency, ca.exchangeBaseUrl); for (let i = 0; i < count; i++) { @@ -455,297 +443,290 @@ export async function getBalancesInsideTransaction( ca.value, ); } - }); + } - await tx.refreshGroups.indexes.byStatus - .iter(keyRangeActive) - .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); - } - }); + const refreshGroups = await tx.getActiveRefreshGroups(); + for (const r of refreshGroups) { + switch (r.operationStatus) { + case RefreshOperationStatus.Pending: + case RefreshOperationStatus.Suspended: + break; + default: + continue; + } + const perExchange = r.infoPerExchange; + if (!perExchange) { + continue; + } + 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 (wg) => { - switch (wg.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.PendingRedenominate: - case WithdrawalGroupStatus.SuspendedRedenominate: - 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.PendingBalanceKyc: - case WithdrawalGroupStatus.SuspendedBalanceKyc: - case WithdrawalGroupStatus.SuspendedKyc: - case WithdrawalGroupStatus.PendingBalanceKycInit: - case WithdrawalGroupStatus.SuspendedBalanceKycInit: - case WithdrawalGroupStatus.PendingKyc: { - checkDbInvariant( - wg.denomsSel !== undefined, - "wg in kyc state should have been initialized", - ); - checkDbInvariant( - wg.exchangeBaseUrl !== undefined, - "wg in kyc state should have been initialized", - ); + const withdrawalGroups = await tx.getActiveWithdrawalGroups(); + for (const wg of withdrawalGroups) { + switch (wg.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 + continue; + case WithdrawalGroupStatus.PendingReady: + case WithdrawalGroupStatus.PendingRedenominate: + case WithdrawalGroupStatus.SuspendedRedenominate: + 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.PendingBalanceKyc: + case WithdrawalGroupStatus.SuspendedBalanceKyc: + case WithdrawalGroupStatus.SuspendedKyc: + case WithdrawalGroupStatus.PendingBalanceKycInit: + case WithdrawalGroupStatus.SuspendedBalanceKycInit: + case WithdrawalGroupStatus.PendingKyc: { + checkDbInvariant( + wg.denomsSel !== undefined, + "wg in kyc state should have been initialized", + ); + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "wg in kyc state should have been initialized", + ); + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl); + break; + } + case WithdrawalGroupStatus.PendingRegisteringBank: { + if (wg.denomsSel && wg.exchangeBaseUrl) { const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); - await balanceStore.setFlagIncomingKyc(currency, wg.exchangeBaseUrl); - break; - } - case WithdrawalGroupStatus.PendingRegisteringBank: { - if (wg.denomsSel && wg.exchangeBaseUrl) { - const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); - await balanceStore.setFlagIncomingConfirmation( - currency, - wg.exchangeBaseUrl, - ); - } - break; - } - case WithdrawalGroupStatus.PendingWaitConfirmBank: { - checkDbInvariant( - wg.exchangeBaseUrl !== undefined, - "withdrawal group in PendingWaitConfirmBank state should have been initialized", - ); - - // FIXME: Consider just having the currency as a fixed field in the DB - // instead of having it in many locations. - let currency: string; - - if (wg.denomsSel) { - currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); - } else if ( - wg.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated && - wg.wgInfo.bankInfo.currency - ) { - currency = wg.wgInfo.bankInfo.currency; - } else { - logger.warn( - "could not determine currency for confirmed withdrawal group", - ); - break; - } await balanceStore.setFlagIncomingConfirmation( currency, wg.exchangeBaseUrl, ); - break; } - default: - assertUnreachable(wg.status); + break; } - if (wg.denomsSel && wg.exchangeBaseUrl) { - // only inform pending incoming if amount and exchange has been selected - const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); - await balanceStore.addPendingIncoming( - currency, - wg.exchangeBaseUrl, - wg.denomsSel.totalCoinValue, + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "withdrawal group in PendingWaitConfirmBank state should have been initialized", ); - } - }); - 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, + // FIXME: Consider just having the currency as a fixed field in the DB + // instead of having it in many locations. + let currency: string; + + if (wg.denomsSel) { + currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + } else if ( + wg.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated && + wg.wgInfo.bankInfo.currency + ) { + currency = wg.wgInfo.bankInfo.currency; + } else { + logger.warn( + "could not determine currency for confirmed withdrawal group", ); break; } - case PeerPushDebitStatus.Failed: - case PeerPushDebitStatus.Aborted: - case PeerPushDebitStatus.Done: - case PeerPushDebitStatus.Expired: - break; - default: - assertUnreachable(ppdRecord.status); + await balanceStore.setFlagIncomingConfirmation( + currency, + wg.exchangeBaseUrl, + ); + break; } - }); + default: + assertUnreachable(wg.status); + } + if (wg.denomsSel && wg.exchangeBaseUrl) { + // only inform pending incoming if amount and exchange has been selected + const currency = Amounts.currencyOf(wg.denomsSel.totalCoinValue); + await balanceStore.addPendingIncoming( + currency, + wg.exchangeBaseUrl, + wg.denomsSel.totalCoinValue, + ); + } + } - await tx.peerPushCredit.indexes.byStatus - .iter(keyRangeActive) - .forEachAsync(async (rec) => { - switch (rec.status) { - case PeerPushCreditStatus.PendingMerge: - case PeerPushCreditStatus.PendingBalanceKycInit: - case PeerPushCreditStatus.PendingBalanceKycRequired: - case PeerPushCreditStatus.SuspendedBalanceKycInit: - case PeerPushCreditStatus.PendingMergeKycRequired: - case PeerPushCreditStatus.SuspendedMerge: - case PeerPushCreditStatus.SuspendedBalanceKycRequired: - case PeerPushCreditStatus.SuspendedMergeKycRequired: { - const currency = Amounts.currencyOf(rec.estimatedAmountEffective); - const amount = rec.estimatedAmountEffective; - await balanceStore.addPendingIncoming( - currency, - rec.exchangeBaseUrl, - amount, - ); - break; - } - case PeerPushCreditStatus.PendingWithdrawing: - case PeerPushCreditStatus.SuspendedWithdrawing: { - // Here, incoming balance should be covered by internal withdrawal tx - break; - } + const peerPushDebits = await tx.getActivePeerPushDebits(); + for (const ppdRecord of peerPushDebits) { + 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; } - }); + case PeerPushDebitStatus.Failed: + case PeerPushDebitStatus.Aborted: + case PeerPushDebitStatus.Done: + case PeerPushDebitStatus.Expired: + break; + default: + assertUnreachable(ppdRecord.status); + } + } - await tx.peerPullCredit.indexes.byStatus - .iter(keyRangeActive) - .forEachAsync(async (rec) => { - switch (rec.status) { - case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: - case PeerPullPaymentCreditStatus.PendingBalanceKycInit: - case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: - case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: - case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingMergeKycRequired: - case PeerPullPaymentCreditStatus.PendingReady: - case PeerPullPaymentCreditStatus.PendingCreatePurse: - case PeerPullPaymentCreditStatus.SuspendedReady: - case PeerPullPaymentCreditStatus.SuspendedCreatePurse: { - const currency = Amounts.currencyOf(rec.amount); - const amount = rec.estimatedAmountEffective; - await balanceStore.addPendingIncoming( - currency, - rec.exchangeBaseUrl, - amount, - ); - break; - } + const peerPushCredits = await tx.getActivePeerPushCredits(); + for (const rec of peerPushCredits) { + switch (rec.status) { + case PeerPushCreditStatus.PendingMerge: + case PeerPushCreditStatus.PendingBalanceKycInit: + case PeerPushCreditStatus.PendingBalanceKycRequired: + case PeerPushCreditStatus.SuspendedBalanceKycInit: + case PeerPushCreditStatus.PendingMergeKycRequired: + case PeerPushCreditStatus.SuspendedMerge: + case PeerPushCreditStatus.SuspendedBalanceKycRequired: + case PeerPushCreditStatus.SuspendedMergeKycRequired: { + const currency = Amounts.currencyOf(rec.estimatedAmountEffective); + const amount = rec.estimatedAmountEffective; + await balanceStore.addPendingIncoming( + currency, + rec.exchangeBaseUrl, + amount, + ); + break; } - }); + case PeerPushCreditStatus.PendingWithdrawing: + case PeerPushCreditStatus.SuspendedWithdrawing: { + // Here, incoming balance should be covered by internal withdrawal tx + break; + } + } + } - await tx.peerPullDebit.indexes.byStatus - .iter(keyRangeActive) - .forEachAsync(async (rec) => { - switch (rec.status) { - case PeerPullDebitRecordStatus.PendingDeposit: - case PeerPullDebitRecordStatus.AbortingRefresh: - case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: - case PeerPullDebitRecordStatus.SuspendedDeposit: - const currency = Amounts.currencyOf(rec.amount); - const amount = rec.coinSel?.totalCost ?? rec.amount; - await balanceStore.addPendingOutgoing( - currency, - rec.exchangeBaseUrl, - amount, - ); - break; + const peerPullCredits = await tx.getActivePeerPullCredits(); + for (const rec of peerPullCredits) { + switch (rec.status) { + case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit: + case PeerPullPaymentCreditStatus.PendingBalanceKycInit: + case PeerPullPaymentCreditStatus.PendingBalanceKycRequired: + case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired: + case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingMergeKycRequired: + case PeerPullPaymentCreditStatus.PendingReady: + case PeerPullPaymentCreditStatus.PendingCreatePurse: + case PeerPullPaymentCreditStatus.SuspendedReady: + case PeerPullPaymentCreditStatus.SuspendedCreatePurse: { + const currency = Amounts.currencyOf(rec.amount); + const amount = rec.estimatedAmountEffective; + await balanceStore.addPendingIncoming( + currency, + rec.exchangeBaseUrl, + amount, + ); + break; } - }); + } + } - await tx.purchases.indexes.byStatus - .iter(keyRangeActive) - .forEachAsync(async (rec) => { - switch (rec.purchaseStatus) { - case PurchaseStatus.SuspendedPaying: - case PurchaseStatus.PendingPaying: - if (!rec.payInfo || !rec.payInfo.payCoinSelection?.coinPubs) { - break; - } - // FIXME: This is pretty slow, cache? - const currency = Amounts.currencyOf(rec.payInfo.totalPayCost); - const sel = rec.payInfo.payCoinSelection; - for (let i = 0; i < sel.coinPubs.length; i++) { - const coinPub = sel.coinPubs[i]; - const coinRec = await tx.coins.get(coinPub); - if (!coinRec) { - continue; - } - await balanceStore.addPendingOutgoing( - currency, - coinRec.exchangeBaseUrl, - sel.coinContributions[i], - ); + const peerPullDebits = await tx.getActivePeerPullDebits(); + for (const rec of peerPullDebits) { + switch (rec.status) { + case PeerPullDebitRecordStatus.PendingDeposit: + case PeerPullDebitRecordStatus.AbortingRefresh: + case PeerPullDebitRecordStatus.SuspendedAbortingRefresh: + case PeerPullDebitRecordStatus.SuspendedDeposit: + const currency = Amounts.currencyOf(rec.amount); + const amount = rec.coinSel?.totalCost ?? rec.amount; + await balanceStore.addPendingOutgoing( + currency, + rec.exchangeBaseUrl, + amount, + ); + break; + } + } + + const purchases = await tx.getActivePurchases(); + for (const rec of purchases) { + switch (rec.purchaseStatus) { + case PurchaseStatus.SuspendedPaying: + case PurchaseStatus.PendingPaying: + if (!rec.payInfo || !rec.payInfo.payCoinSelection?.coinPubs) { + break; + } + const currency = Amounts.currencyOf(rec.payInfo.totalPayCost); + const sel = rec.payInfo.payCoinSelection; + const coins = await tx.getCoinsByPubs(sel.coinPubs); + const coinsByPub = new Map(coins.map((c) => [c.coinPub, c])); + for (let i = 0; i < sel.coinPubs.length; i++) { + const coinPub = sel.coinPubs[i]; + const coinRec = coinsByPub.get(coinPub); + if (!coinRec) { + continue; } - } - }); + await balanceStore.addPendingOutgoing( + currency, + coinRec.exchangeBaseUrl, + sel.coinContributions[i], + ); + } + } + } - await tx.depositGroups.indexes.byStatus - .iter(keyRangeActive) - .forEachAsync(async (dgRecord) => { - const perExchange = dgRecord.infoPerExchange; - if (!perExchange) { - return; + const activeDepositGroups = await tx.getActiveDepositGroups(); + for (const dgRecord of activeDepositGroups) { + const perExchange = dgRecord.infoPerExchange; + if (!perExchange) { + continue; + } + for (const [e, x] of Object.entries(perExchange)) { + const currency = Amounts.currencyOf(dgRecord.amount); + switch (dgRecord.operationStatus) { + case DepositOperationStatus.SuspendedAggregateKyc: + case DepositOperationStatus.PendingAggregateKyc: + case DepositOperationStatus.PendingDepositKyc: + case DepositOperationStatus.PendingDepositKycAuth: + case DepositOperationStatus.SuspendedDepositKyc: + case DepositOperationStatus.SuspendedDepositKycAuth: + await balanceStore.setFlagOutgoingKyc(currency, e); } - for (const [e, x] of Object.entries(perExchange)) { - const currency = Amounts.currencyOf(dgRecord.amount); - switch (dgRecord.operationStatus) { - case DepositOperationStatus.SuspendedAggregateKyc: - case DepositOperationStatus.PendingAggregateKyc: - case DepositOperationStatus.PendingDepositKyc: - case DepositOperationStatus.PendingDepositKycAuth: - case DepositOperationStatus.SuspendedDepositKyc: - case DepositOperationStatus.SuspendedDepositKycAuth: - await balanceStore.setFlagOutgoingKyc(currency, e); - } - switch (dgRecord.operationStatus) { - case DepositOperationStatus.SuspendedAggregateKyc: - case DepositOperationStatus.PendingAggregateKyc: - case DepositOperationStatus.LegacyPendingTrack: - case DepositOperationStatus.SuspendedAborting: - case DepositOperationStatus.SuspendedDeposit: - case DepositOperationStatus.LegacySuspendedTrack: - case DepositOperationStatus.PendingDepositKyc: - case DepositOperationStatus.PendingDepositKycAuth: - case DepositOperationStatus.SuspendedDepositKyc: - case DepositOperationStatus.SuspendedDepositKycAuth: - case DepositOperationStatus.PendingDeposit: { - const perExchange = dgRecord.infoPerExchange; - if (perExchange) { - for (const [e, v] of Object.entries(perExchange)) { - await balanceStore.addPendingOutgoing( - currency, - e, - v.amountEffective, - ); - } + switch (dgRecord.operationStatus) { + case DepositOperationStatus.SuspendedAggregateKyc: + case DepositOperationStatus.PendingAggregateKyc: + case DepositOperationStatus.LegacyPendingTrack: + case DepositOperationStatus.SuspendedAborting: + case DepositOperationStatus.SuspendedDeposit: + case DepositOperationStatus.LegacySuspendedTrack: + case DepositOperationStatus.PendingDepositKyc: + case DepositOperationStatus.PendingDepositKycAuth: + case DepositOperationStatus.SuspendedDepositKyc: + case DepositOperationStatus.SuspendedDepositKycAuth: + 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(); } @@ -758,7 +739,7 @@ export async function getBalances( ): Promise<BalancesResponse> { logger.trace("starting to compute balance"); - const wbal = await wex.runLegacyWalletDbTx(async (tx) => { + const wbal = await wex.runWalletDbTx(async (tx) => { return getBalancesInsideTransaction(wex, tx); }); @@ -776,7 +757,7 @@ export async function getBalanceOfScope( ): Promise<BalancesResponse> { logger.trace("starting to compute balance"); - const wbal = await wex.runLegacyWalletDbTx(async (tx) => { + const wbal = await wex.runWalletDbTx(async (tx) => { return getBalancesInsideTransaction(wex, tx); }); @@ -885,16 +866,17 @@ export async function getPaymentBalanceDetails( wex: WalletExecutionContext, req: PaymentRestrictionsForBalance, ): Promise<PaymentBalanceDetails> { - return await wex.runLegacyWalletDbTx(async (tx) => { + return await wex.runWalletDbTx(async (tx) => { return getPaymentBalanceDetailsInTx(wex, tx, req); }); } export async function getPaymentBalanceDetailsInTx( wex: WalletExecutionContext, - tx: LegacyWalletTxHandle, + tx: WalletDbTransaction | LegacyWalletTxHandle, req: PaymentRestrictionsForBalance, ): Promise<PaymentBalanceDetails> { + const dbTx = "wtx" in tx ? tx.wtx : tx; const d: PaymentBalanceDetails = { balanceAvailable: Amounts.zeroOfCurrency(req.currency), balanceMaterial: Amounts.zeroOfCurrency(req.currency), @@ -910,7 +892,7 @@ export async function getPaymentBalanceDetailsInTx( logger.trace(`computing balance details for ${j2s(req)}`); - const availableCoins = await tx.coinAvailability.getAll(); + const availableCoins = await dbTx.getCoinAvailabilities(); for (const ca of availableCoins) { if (ca.currency != req.currency) { @@ -919,7 +901,7 @@ export async function getPaymentBalanceDetailsInTx( const denom = await getDenomInfo( wex, - tx, + dbTx, ca.exchangeBaseUrl, ca.denomPubHash, ); @@ -930,8 +912,7 @@ export async function getPaymentBalanceDetailsInTx( // Skip exchanges if excluded by the receiver. if ( req.restrictSenderScope && - !(await checkExchangeInScopeTx( - tx, + !(await dbTx.checkExchangeInScope( ca.exchangeBaseUrl, req.restrictSenderScope, )) @@ -939,7 +920,7 @@ export async function getPaymentBalanceDetailsInTx( continue; } - const wireDetails = await getExchangeDetailsInTx(tx, ca.exchangeBaseUrl); + const wireDetails = await dbTx.getExchangeDetails(ca.exchangeBaseUrl); if (!wireDetails) { continue; } @@ -1066,16 +1047,17 @@ export async function getPaymentBalanceDetailsInTx( } } - await tx.refreshGroups.iter().forEach((r) => { + const refreshGroups = await dbTx.getActiveRefreshGroups(); + for (const r of refreshGroups) { if (r.currency != req.currency) { - return; + continue; } const balRefresh = computeRefreshGroupAvailableAmountForExchanges( r, req.restrictReceiverExchanges?.exchanges, ); d.balanceAvailable = Amounts.add(d.balanceAvailable, balRefresh).amount; - }); + } return d; } @@ -1086,10 +1068,10 @@ export async function getBalanceDetail( ): Promise<PaymentBalanceDetails> { const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = []; const wires = new Array<string>(); - await wex.runLegacyWalletDbTx(async (tx) => { - const allExchanges = await tx.exchanges.iter().toArray(); + await wex.runWalletDbTx(async (tx) => { + const allExchanges = await tx.getExchanges(); for (const e of allExchanges) { - const details = await getExchangeDetailsInTx(tx, e.baseUrl); + const details = await tx.getExchangeDetails(e.baseUrl); if (!details || req.currency !== details.currency) { continue; } diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts @@ -55,6 +55,7 @@ import { SelectedCoin, strcmp, TalerProtocolTimestamp, + WireInfo, } from "@gnu-taler/taler-util"; import { getPaymentBalanceDetailsInTx, @@ -837,7 +838,7 @@ export type AvailableCoinsOfDenom = DenominationInfo & { export function findMatchingWire( wireMethod: string, depositPaytoUri: string | undefined, - exchangeWireDetails: ExchangeDetails, + exchangeWireDetails: { wireInfo: WireInfo }, ): | { ok: true; wireFee: AmountJson } | { ok: false; accountRestrictions: Record<string, AccountRestriction[]> } diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts @@ -26,6 +26,9 @@ import { MerchantContractTokenDetails, ScopeInfo, TalerErrorDetail, + DenominationPubKey, + Amounts, + DenominationInfo, } from "@gnu-taler/taler-util"; declare const symDbProtocolTimestamp: unique symbol; @@ -1082,3 +1085,140 @@ export interface WalletToken { */ tokenUseSig?: TokenUseSig; } + +/** + * Denomination record as stored in the wallet's database. + */ +export interface DenominationRecord { + /** + * Currency of the denomination. + * + * Stored separately as we have an index on it. + */ + currency: string; + + value: AmountString; + + /** + * The denomination public key. + */ + denomPub: DenominationPubKey; + + /** + * Hash of the denomination public key. + * Stored in the database for faster lookups. + */ + denomPubHash: string; + + fees: DenomFees; + + denominationFamilySerial: number; + + /** + * Validity start date of the denomination. + */ + stampStart: DbProtocolTimestamp; + + /** + * Date after which the currency can't be withdrawn anymore. + */ + stampExpireWithdraw: DbProtocolTimestamp; + + /** + * Date after the denomination officially doesn't exist anymore. + */ + stampExpireLegal: DbProtocolTimestamp; + + /** + * Data after which coins of this denomination can't be deposited anymore. + */ + stampExpireDeposit: DbProtocolTimestamp; + + /** + * Signature by the exchange's master key over the denomination + * information. + */ + masterSig: string; + + /** + * Did we verify the signature on the denomination? + */ + verificationStatus: DenominationVerificationStatus; + + /** + * Was this denomination still offered by the exchange the last time + * we checked? + * Only false when the exchange redacts a previously published denomination. + */ + isOffered: boolean; + + /** + * Did the exchange revoke the denomination? + * When this field is set to true in the database, the same transaction + * should also mark all affected coins as revoked. + */ + isRevoked: boolean; + + /** + * If set to true, the exchange announced that the private key for this + * denomination is lost. Thus it can't be used to sign new coins + * during withdrawal/refresh/..., but the coins can still be spent. + */ + isLost?: boolean; + + /** + * Base URL of the exchange. + */ + exchangeBaseUrl: string; + + /** + * Master public key of the exchange that made the signature + * on the denomination. + */ + exchangeMasterPub: string; +} + +export interface DenomFees { + /** + * Fee for withdrawing. + */ + feeWithdraw: AmountString; + + /** + * Fee for depositing. + */ + feeDeposit: AmountString; + + /** + * Fee for refreshing. + */ + feeRefresh: AmountString; + + /** + * Fee for refunding. + */ + feeRefund: AmountString; +} + +export namespace DenominationRecord { + export function toDenomInfo(d: DenominationRecord): DenominationInfo { + return { + denomPub: d.denomPub, + exchangeMasterPub: d.exchangeMasterPub, + denomPubHash: d.denomPubHash, + feeDeposit: Amounts.stringify(d.fees.feeDeposit), + feeRefresh: Amounts.stringify(d.fees.feeRefresh), + feeRefund: Amounts.stringify(d.fees.feeRefund), + feeWithdraw: Amounts.stringify(d.fees.feeWithdraw), + stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit), + stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal), + stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw), + stampStart: timestampProtocolFromDb(d.stampStart), + value: Amounts.stringify(d.value), + exchangeBaseUrl: d.exchangeBaseUrl, + isLost: d.isLost ?? false, + masterSig: d.masterSig, + isOffered: d.isOffered, + }; + } +} diff --git a/packages/taler-wallet-core/src/db-indexeddb.ts b/packages/taler-wallet-core/src/db-indexeddb.ts @@ -110,7 +110,10 @@ import { WalletBackupConfState, WithdrawalGroupStatus, timestampProtocolFromDb, + DenominationRecord, + DenomFees, } from "./db-common.js"; +export { DenominationRecord, DenomFees }; import { DbAccess, DbAccessImpl, @@ -254,28 +257,6 @@ export interface ReserveBankInfo { senderWire?: string; } -export interface DenomFees { - /** - * Fee for withdrawing. - */ - feeWithdraw: AmountString; - - /** - * Fee for depositing. - */ - feeDeposit: AmountString; - - /** - * Fee for refreshing. - */ - feeRefresh: AmountString; - - /** - * Fee for refunding. - */ - feeRefund: AmountString; -} - export interface DenominationFamilyRecord { denominationFamilySerial?: number; familyParams: DenomFamilyParams; @@ -287,117 +268,6 @@ export interface DenominationFamilyRecord { /** * Denomination record as stored in the wallet's database. */ -export interface DenominationRecord { - /** - * Currency of the denomination. - * - * Stored separately as we have an index on it. - */ - currency: string; - - value: AmountString; - - /** - * The denomination public key. - */ - denomPub: DenominationPubKey; - - /** - * Hash of the denomination public key. - * Stored in the database for faster lookups. - */ - denomPubHash: string; - - fees: DenomFees; - - denominationFamilySerial: number; - - /** - * Validity start date of the denomination. - */ - stampStart: DbProtocolTimestamp; - - /** - * Date after which the currency can't be withdrawn anymore. - */ - stampExpireWithdraw: DbProtocolTimestamp; - - /** - * Date after the denomination officially doesn't exist anymore. - */ - stampExpireLegal: DbProtocolTimestamp; - - /** - * Data after which coins of this denomination can't be deposited anymore. - */ - stampExpireDeposit: DbProtocolTimestamp; - - /** - * Signature by the exchange's master key over the denomination - * information. - */ - masterSig: string; - - /** - * Did we verify the signature on the denomination? - */ - verificationStatus: DenominationVerificationStatus; - - /** - * Was this denomination still offered by the exchange the last time - * we checked? - * Only false when the exchange redacts a previously published denomination. - */ - isOffered: boolean; - - /** - * Did the exchange revoke the denomination? - * When this field is set to true in the database, the same transaction - * should also mark all affected coins as revoked. - */ - isRevoked: boolean; - - /** - * If set to true, the exchange announced that the private key for this - * denomination is lost. Thus it can't be used to sign new coins - * during withdrawal/refresh/..., but the coins can still be spent. - */ - isLost?: boolean; - - /** - * Base URL of the exchange. - */ - exchangeBaseUrl: string; - - /** - * Master public key of the exchange that made the signature - * on the denomination. - */ - exchangeMasterPub: string; -} - -export namespace DenominationRecord { - export function toDenomInfo(d: DenominationRecord): DenominationInfo { - return { - denomPub: d.denomPub, - exchangeMasterPub: d.exchangeMasterPub, - denomPubHash: d.denomPubHash, - feeDeposit: Amounts.stringify(d.fees.feeDeposit), - feeRefresh: Amounts.stringify(d.fees.feeRefresh), - feeRefund: Amounts.stringify(d.fees.feeRefund), - feeWithdraw: Amounts.stringify(d.fees.feeWithdraw), - stampExpireDeposit: timestampProtocolFromDb(d.stampExpireDeposit), - stampExpireLegal: timestampProtocolFromDb(d.stampExpireLegal), - stampExpireWithdraw: timestampProtocolFromDb(d.stampExpireWithdraw), - stampStart: timestampProtocolFromDb(d.stampStart), - value: Amounts.stringify(d.value), - exchangeBaseUrl: d.exchangeBaseUrl, - isLost: d.isLost ?? false, - masterSig: d.masterSig, - isOffered: d.isOffered, - }; - } -} export interface ExchangeSignkeysRecord { stampStart: DbProtocolTimestamp; diff --git a/packages/taler-wallet-core/src/dbtx.ts b/packages/taler-wallet-core/src/dbtx.ts @@ -21,7 +21,10 @@ import { MailboxMessageRecord, ScopeInfo, stringifyScopeInfo, + assertUnreachable, + ScopeType, } from "@gnu-taler/taler-util"; +import { GlobalIDB } from "@gnu-taler/idb-bridge"; import { ConfigRecord, WalletPeerPullCredit, @@ -29,7 +32,21 @@ import { WalletPeerPushCredit, WalletPeerPullDebit, WalletToken, + DenominationRecord, + DenominationVerificationStatus, + OPERATION_STATUS_NONFINAL_FIRST, + OPERATION_STATUS_NONFINAL_LAST, } from "./db-common.js"; +import type { + ExchangeEntryRecord, + CoinAvailabilityRecord, + RefreshGroupRecord, + WithdrawalGroupRecord, + CoinRecord, + DepositGroupRecord, + ExchangeDetailsRecord, + DonationSummaryRecord, +} from "./db-indexeddb.js"; import { PeerPullCreditRecord, PeerPushDebitRecord, @@ -198,6 +215,62 @@ export interface WalletDbTransaction { exchangeBaseUrl: string, contractPriv: string, ): Promise<WalletPeerPullDebit | undefined>; + + upsertDenomination(rec: DenominationRecord): Promise<void>; + + getDenomination( + exchangeBaseUrl: string, + denomPubHash: string, + ): Promise<DenominationRecord | undefined>; + + getDenominationsByVerificationStatus( + verificationStatus: DenominationVerificationStatus, + ): Promise<DenominationRecord[]>; + + getDonationSummaries(): Promise<DonationSummaryRecord[]>; + + getExchanges(): Promise<ExchangeEntryRecord[]>; + + getCoinAvailabilities(): Promise<CoinAvailabilityRecord[]>; + + getActiveRefreshGroups(): Promise<RefreshGroupRecord[]>; + + getActiveWithdrawalGroups(): Promise<WithdrawalGroupRecord[]>; + + getActivePeerPushDebits(): Promise<PeerPushDebitRecord[]>; + + getActivePeerPushCredits(): Promise<PeerPushCreditRecord[]>; + + getActivePeerPullCredits(): Promise<PeerPullCreditRecord[]>; + + getActivePeerPullDebits(): Promise<PeerPullPaymentIncomingRecord[]>; + + getActivePurchases(): Promise<PurchaseRecord[]>; + + getCoinsByPubs(coinPubs: string[]): Promise<CoinRecord[]>; + + getActiveDepositGroups(): Promise<DepositGroupRecord[]>; + + getExchangeDetails( + exchangeBaseUrl: string, + ): Promise<ExchangeDetailsRecord | undefined>; + + checkExchangeInScope( + exchangeBaseUrl: string, + scope: ScopeInfo, + ): Promise<boolean>; + + getExchangeScopeInfo( + exchangeBaseUrl: string, + currency: string, + ): Promise<ScopeInfo>; +} + +function getActiveKeyRange() { + return GlobalIDB.KeyRange.bound( + OPERATION_STATUS_NONFINAL_FIRST, + OPERATION_STATUS_NONFINAL_LAST, + ); } export class IdbWalletTransaction implements WalletDbTransaction { @@ -661,4 +734,184 @@ export class IdbWalletTransaction implements WalletDbTransaction { } return this.getPeerPullDebit(r.peerPullDebitId); } + + async upsertDenomination(rec: DenominationRecord): Promise<void> { + const tx = this.tx; + await tx.denominations.put(rec); + } + + async getDenomination( + exchangeBaseUrl: string, + denomPubHash: string, + ): Promise<DenominationRecord | undefined> { + const tx = this.tx; + return await tx.denominations.get([exchangeBaseUrl, denomPubHash]); + } + + async getDenominationsByVerificationStatus( + verificationStatus: DenominationVerificationStatus, + ): Promise<DenominationRecord[]> { + const tx = this.tx; + return await tx.denominations.indexes.byVerificationStatus.getAll( + verificationStatus, + ); + } + + async getDonationSummaries(): Promise<DonationSummaryRecord[]> { + return await this.tx.donationSummaries.iter().toArray(); + } + + async getExchanges(): Promise<ExchangeEntryRecord[]> { + return await this.tx.exchanges.iter().toArray(); + } + + async getCoinAvailabilities(): Promise<CoinAvailabilityRecord[]> { + return await this.tx.coinAvailability.iter().toArray(); + } + + async getActiveRefreshGroups(): Promise<RefreshGroupRecord[]> { + return await this.tx.refreshGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActiveWithdrawalGroups(): Promise<WithdrawalGroupRecord[]> { + return await this.tx.withdrawalGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPushDebits(): Promise<PeerPushDebitRecord[]> { + return await this.tx.peerPushDebit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPushCredits(): Promise<PeerPushCreditRecord[]> { + return await this.tx.peerPushCredit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPullCredits(): Promise<PeerPullCreditRecord[]> { + return await this.tx.peerPullCredit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePeerPullDebits(): Promise<PeerPullPaymentIncomingRecord[]> { + return await this.tx.peerPullDebit.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getActivePurchases(): Promise<PurchaseRecord[]> { + return await this.tx.purchases.indexes.byStatus.getAll(getActiveKeyRange()); + } + + async getCoinsByPubs(coinPubs: string[]): Promise<CoinRecord[]> { + const coins: CoinRecord[] = []; + for (const pub of coinPubs) { + const coin = await this.tx.coins.get(pub); + if (coin) { + coins.push(coin); + } + } + return coins; + } + + async getActiveDepositGroups(): Promise<DepositGroupRecord[]> { + return await this.tx.depositGroups.indexes.byStatus.getAll( + getActiveKeyRange(), + ); + } + + async getExchangeDetails( + exchangeBaseUrl: string, + ): Promise<ExchangeDetailsRecord | undefined> { + const r = await this.tx.exchanges.get(exchangeBaseUrl); + if (!r || !r.detailsPointer) { + return undefined; + } + return await this.tx.exchangeDetails.indexes.byPointer.get([ + r.baseUrl, + r.detailsPointer.currency, + r.detailsPointer.masterPublicKey, + ]); + } + + async checkExchangeInScope( + exchangeBaseUrl: string, + scope: ScopeInfo, + ): Promise<boolean> { + switch (scope.type) { + case ScopeType.Exchange: { + return scope.url === exchangeBaseUrl; + } + case ScopeType.Global: { + const exchangeDetails = await this.getExchangeDetails(exchangeBaseUrl); + if (!exchangeDetails) { + return false; + } + const gr = + await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get( + [ + exchangeDetails.currency, + exchangeBaseUrl, + exchangeDetails.masterPublicKey, + ], + ); + return gr != null; + } + case ScopeType.Auditor: + throw Error("auditor scope not supported yet"); + default: + assertUnreachable(scope); + } + } + + async getExchangeScopeInfo( + exchangeBaseUrl: string, + currency: string, + ): Promise<ScopeInfo> { + const det = await this.getExchangeDetails(exchangeBaseUrl); + if (!det) { + return { + type: ScopeType.Exchange, + currency: currency, + url: exchangeBaseUrl, + }; + } + const globalExchangeRec = + await this.tx.globalCurrencyExchanges.indexes.byCurrencyAndUrlAndPub.get([ + det.currency, + det.exchangeBaseUrl, + det.masterPublicKey, + ]); + if (globalExchangeRec) { + return { + currency: det.currency, + type: ScopeType.Global, + }; + } else { + for (const aud of det.auditors) { + const globalAuditorRec = + await this.tx.globalCurrencyAuditors.indexes.byCurrencyAndUrlAndPub.get( + [det.currency, aud.auditor_url, aud.auditor_pub], + ); + if (globalAuditorRec) { + return { + currency: det.currency, + type: ScopeType.Auditor, + url: aud.auditor_url, + }; + } + } + } + return { + currency: det.currency, + type: ScopeType.Exchange, + url: det.exchangeBaseUrl, + }; + } } diff --git a/packages/taler-wallet-core/src/denominations.ts b/packages/taler-wallet-core/src/denominations.ts @@ -35,8 +35,9 @@ import { TaskRunResult } from "./common.js"; import { DenominationVerificationStatus, timestampProtocolFromDb, + DenominationRecord, } from "./db-common.js"; -import { DenominationRecord } from "./db-indexeddb.js"; +import { WalletDbTransaction } from "./dbtx.js"; import { WalletExecutionContext } from "./index.js"; /** @@ -622,10 +623,10 @@ export async function validateDenoms( if (updatedDenoms.length > 0) { logger.trace("writing denomination batch to db"); - await wex.runLegacyWalletDbTx(async (tx) => { + await wex.runWalletDbTx(async (tx) => { for (let i = 0; i < updatedDenoms.length; i++) { const denom = updatedDenoms[i]; - await tx.denominations.put(denom); + await tx.upsertDenomination(denom); } }); @@ -640,8 +641,8 @@ export async function processValidateDenoms( ): Promise<TaskRunResult> { logger.trace("validating denoms in background task"); - const denoms = await wex.runLegacyWalletDbTx(async (tx) => { - return tx.denominations.indexes.byVerificationStatus.getAll( + const denoms = await wex.runWalletDbTx(async (tx) => { + return tx.getDenominationsByVerificationStatus( DenominationVerificationStatus.Unverified, ); }); diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -478,8 +478,8 @@ export interface ExchangeDetails { auditors: ExchangeAuditor[]; globalFees: ExchangeGlobalFees[]; reserveClosingDelay: TalerProtocolDuration; - defaultPeerPushExpiration: TalerProtocolDuration | undefined; - shoppingUrl: string | undefined; + defaultPeerPushExpiration?: TalerProtocolDuration; + shoppingUrl?: string; } export async function getExchangeDetailsInTx( diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -601,13 +601,16 @@ async function migrateMaterializedTransactions( export async function getDenomInfo( wex: WalletExecutionContext, - tx: WalletIndexedDbTransaction, + tx: WalletDbTransaction | LegacyWalletTxHandle, exchangeBaseUrl: string, denomPubHash: string, ): Promise<DenominationInfo | undefined> { const key = `${exchangeBaseUrl}:${denomPubHash}`; return wex.ws.denomInfoCache.getOrPut(key, async () => { - const d = await tx.denominations.get([exchangeBaseUrl, denomPubHash]); + const d = + "getDenomination" in tx + ? await tx.getDenomination(exchangeBaseUrl, denomPubHash) + : await tx.denominations.get([exchangeBaseUrl, denomPubHash]); if (d != null) { return DenominationRecord.toDenomInfo(d); } else {