Changeset View
Changeset View
Standalone View
Standalone View
apps/marlin-wallet/web/src/transaction-history.ts
| // Copyright (c) 2025 The Bitcoin developers | // Copyright (c) 2025 The Bitcoin developers | ||||
| // Distributed under the MIT software license, see the accompanying | // Distributed under the MIT software license, see the accompanying | ||||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||||
| import { webViewLog, webViewError } from './common'; | import { webViewLog, webViewError } from './common'; | ||||
| import { calculateTransactionAmountSats, satsToXec } from './amount'; | import { calculateTransactionAmountAtoms, atomsToUnit } from './amount'; | ||||
| import { Wallet } from 'ecash-wallet'; | import { Wallet } from 'ecash-wallet'; | ||||
| import { ChronikClient } from 'chronik-client'; | import { ChronikClient } from 'chronik-client'; | ||||
| import { getAddress } from './wallet'; | import { getAddress } from './wallet'; | ||||
| import { AppSettings } from './settings'; | import { AppSettings } from './settings'; | ||||
| import { CryptoTicker, formatPrice } from 'ecash-price'; | import { formatPrice } from 'ecash-price'; | ||||
| import type { MarlinPriceFetcher } from './price'; | import type { MarlinPriceFetcher } from './price'; | ||||
| import { | |||||
| activeCryptoTicker, | |||||
| activeAssetDecimals, | |||||
| activeTokenId, | |||||
| allowFiatForActiveAsset, | |||||
| activeQuoteCurrency, | |||||
| } from './active-asset'; | |||||
| // ============================================================================ | // ============================================================================ | ||||
| // TRANSACTION HISTORY MANAGER | // TRANSACTION HISTORY MANAGER | ||||
| // ============================================================================ | // ============================================================================ | ||||
| // Transaction History Manager | // Transaction History Manager | ||||
| export class TransactionHistoryManager { | export class TransactionHistoryManager { | ||||
| private currentPage = 0; | private currentPage = 0; | ||||
| ▲ Show 20 Lines • Show All 144 Lines • ▼ Show 20 Lines | async displayTransactions(transactions: any[]): Promise<void> { | ||||
| const transactionList = document.getElementById('transaction-list'); | const transactionList = document.getElementById('transaction-list'); | ||||
| if (!transactionList) return; | if (!transactionList) return; | ||||
| if (transactions.length === 0) { | if (transactions.length === 0) { | ||||
| this.showNoTransactions(); | this.showNoTransactions(); | ||||
| return; | return; | ||||
| } | } | ||||
| // Fetch price once for fiat conversion | const pricePerXec = allowFiatForActiveAsset() | ||||
| const pricePerXec = await this.priceFetcher?.current({ | ? await this.priceFetcher?.current({ | ||||
| source: CryptoTicker.XEC, | source: activeQuoteCurrency(), | ||||
| quote: this.appSettings.fiatCurrency, | quote: this.appSettings.fiatCurrency, | ||||
| }); | }) | ||||
| : null; | |||||
| const tokenId = activeTokenId(); | |||||
| const primaryTicker = activeCryptoTicker(); | |||||
| const tokenDecimals = activeAssetDecimals(); | |||||
| const xecFormatOptions = { | |||||
| locale: this.appSettings.locale, | |||||
| decimals: tokenDecimals, | |||||
| alwaysShowSign: true, | |||||
| }; | |||||
| const fiatFormatOptions = { | |||||
| locale: this.appSettings.locale, | |||||
| alwaysShowSign: true, | |||||
| }; | |||||
| // Process transactions in parallel for better performance | // Process transactions in parallel for better performance | ||||
| const transactionHTML = await Promise.all( | const transactionHTML = await Promise.all( | ||||
| transactions.map(async tx => { | transactions.map(async tx => { | ||||
| // Use timeFirstSeen field from Chronik, fallback to block timestamp if zero | // Use timeFirstSeen field from Chronik, fallback to block timestamp if zero | ||||
| let time; | let time; | ||||
| if (tx.timeFirstSeen && tx.timeFirstSeen > 0) { | if (tx.timeFirstSeen && tx.timeFirstSeen > 0) { | ||||
| time = new Date(tx.timeFirstSeen * 1000).toLocaleString( | time = new Date(tx.timeFirstSeen * 1000).toLocaleString( | ||||
| Show All 9 Lines | async displayTransactions(transactions: any[]): Promise<void> { | ||||
| const txid = String(tx.txid); | const txid = String(tx.txid); | ||||
| const shortTxid = | const shortTxid = | ||||
| txid.length > 12 | txid.length > 12 | ||||
| ? `${txid.substring(0, 6)}...${txid.substring( | ? `${txid.substring(0, 6)}...${txid.substring( | ||||
| txid.length - 6, | txid.length - 6, | ||||
| )}` | )}` | ||||
| : txid; | : txid; | ||||
| // Calculate real transaction amount | const atoms = await calculateTransactionAmountAtoms( | ||||
| const amountSats = await calculateTransactionAmountSats( | |||||
| this.ecashWallet, | this.ecashWallet, | ||||
| this.chronik, | this.chronik, | ||||
| txid, | txid, | ||||
| tokenId, | |||||
| ); | ); | ||||
| const amountXEC = satsToXec(amountSats); | |||||
| const isReceived = amountXEC >= 0; | if (tokenId !== null && atoms === 0) { | ||||
| const amountClass = isReceived ? 'received' : 'sent'; | return null; | ||||
| } | |||||
| const xecFormatOptions = { | const amountPrimary = atomsToUnit(atoms, activeAssetDecimals()); | ||||
| locale: this.appSettings.locale, | |||||
| decimals: 2, | const isReceived = amountPrimary >= 0; | ||||
| alwaysShowSign: true, | const amountClass = isReceived ? 'received' : 'sent'; | ||||
| }; | |||||
| const fiatFormatOptions = { | |||||
| locale: this.appSettings.locale, | |||||
| alwaysShowSign: true, | |||||
| }; | |||||
| // Format primary amount according to primary balance type. | |||||
| // If the price is not available, always show as XEC. | |||||
| const primaryAmount = | const primaryAmount = | ||||
| this.appSettings.primaryBalanceType === 'XEC' || | this.appSettings.primaryBalanceType === 'XEC' || | ||||
| pricePerXec === null | pricePerXec === null | ||||
| ? formatPrice( | ? formatPrice( | ||||
| amountXEC, | amountPrimary, | ||||
| CryptoTicker.XEC, | primaryTicker, | ||||
| xecFormatOptions, | xecFormatOptions, | ||||
| ) | ) | ||||
| : formatPrice( | : formatPrice( | ||||
| amountXEC * pricePerXec, | amountPrimary * pricePerXec, | ||||
| this.appSettings.fiatCurrency, | this.appSettings.fiatCurrency, | ||||
| fiatFormatOptions, | fiatFormatOptions, | ||||
| ); | ); | ||||
| // Format secondary amount if we have a price | |||||
| let secondaryAmount: string = ''; | let secondaryAmount: string = ''; | ||||
| if (pricePerXec !== null) { | if (pricePerXec !== null) { | ||||
| secondaryAmount = | secondaryAmount = | ||||
| this.appSettings.primaryBalanceType === 'XEC' | this.appSettings.primaryBalanceType === 'XEC' | ||||
| ? formatPrice( | ? formatPrice( | ||||
| amountXEC * pricePerXec, | amountPrimary * pricePerXec, | ||||
| this.appSettings.fiatCurrency, | this.appSettings.fiatCurrency, | ||||
| fiatFormatOptions, | fiatFormatOptions, | ||||
| ) | ) | ||||
| : formatPrice( | : formatPrice( | ||||
| amountXEC, | amountPrimary, | ||||
| CryptoTicker.XEC, | primaryTicker, | ||||
| xecFormatOptions, | xecFormatOptions, | ||||
| ); | ); | ||||
| } | } | ||||
| return ` | return ` | ||||
| <div class="transaction-item"> | <div class="transaction-item"> | ||||
| <div class="transaction-info"> | <div class="transaction-info"> | ||||
| <div class="transaction-time">${String(time)}</div> | <div class="transaction-time">${String(time)}</div> | ||||
| Show All 18 Lines | async displayTransactions(transactions: any[]): Promise<void> { | ||||
| loadingIndicator = ` | loadingIndicator = ` | ||||
| <div class="loading-more"> | <div class="loading-more"> | ||||
| <div class="loading-spinner"></div> | <div class="loading-spinner"></div> | ||||
| <p>Loading more transactions...</p> | <p>Loading more transactions...</p> | ||||
| </div> | </div> | ||||
| `; | `; | ||||
| } | } | ||||
| transactionList.innerHTML = transactionHTML.join('') + loadingIndicator; | transactionList.innerHTML = | ||||
| transactionHTML.filter(t => t !== null).join('') + loadingIndicator; | |||||
| } | } | ||||
| // Show no transactions message | // Show no transactions message | ||||
| showNoTransactions(): void { | showNoTransactions(): void { | ||||
| const transactionList = document.getElementById('transaction-list'); | const transactionList = document.getElementById('transaction-list'); | ||||
| if (!transactionList) { | if (!transactionList) { | ||||
| return; | return; | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 42 Lines • Show Last 20 Lines | |||||