From 9c303e8c23391162c38d9eaf6ea83eb7ff7fe365 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 29 Sep 2024 09:11:40 +0000 Subject: [PATCH] address wallet page by name --- .../addresses-treemap.component.ts | 23 +- .../custom-dashboard.component.ts | 52 +- .../components/wallet/wallet.component.html | 92 +-- .../app/components/wallet/wallet.component.ts | 543 ++++++++---------- .../src/app/graphs/graphs.routing.module.ts | 2 +- .../src/app/interfaces/node-api.interface.ts | 5 +- frontend/src/app/services/state.service.ts | 2 +- 7 files changed, 319 insertions(+), 400 deletions(-) diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts index 705941caf..f78b4e2e1 100644 --- a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts @@ -39,14 +39,19 @@ export class AddressesTreemap implements OnChanges { } prepareChartOptions(): void { - const maxTxs = this.addresses.reduce((max, address) => Math.max(max, address.chain_stats.tx_count), 0); const data = this.addresses.map(address => ({ - address: address.address, - value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum, - stats: address.chain_stats, - itemStyle: { - color: lerpColor('#1E88E5', '#D81B60', address.chain_stats.tx_count / maxTxs), - } + address: address.address, + value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum, + stats: address.chain_stats, + })); + // only consider visible items for the color gradient + const totalValue = data.reduce((acc, address) => acc + address.value, 0); + const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0); + const dataItems = data.map(address => ({ + ...address, + itemStyle: { + color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs), + } })); this.chartOptions = { tooltip: { @@ -64,7 +69,7 @@ export class AddressesTreemap implements OnChanges { top: 0, roam: false, type: 'treemap', - data: data, + data: dataItems, nodeClick: 'link', progressive: 100, tooltip: { @@ -87,7 +92,7 @@ export class AddressesTreemap implements OnChanges { ${value.data.address} - Recieved + Received ${this.formatValue(value.data.stats.funded_txo_sum)} diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts index 622e6cf3a..eb9818632 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts @@ -370,23 +370,47 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet; this.websocketService.startTrackingWallet(walletName); - this.walletSummary$ = this.apiService.getWallet$(walletName).pipe( + this.walletSummary$ = this.apiService.getWallet$(walletName).pipe( catchError(e => { - return of(null); + return of({}); }), - map((walletTransactions) => { - const transactions = Object.values(walletTransactions).flatMap(wallet => wallet.transactions); - return this.deduplicateWalletTransactions(transactions); - }), - switchMap(initial => this.stateService.walletTransactions$.pipe( - startWith(null), - scan((summary, walletTransactions) => { - if (walletTransactions) { - const transactions: AddressTxSummary[] = [...summary, ...Object.values(walletTransactions).flat()]; - return this.deduplicateWalletTransactions(transactions); + switchMap(wallet => this.stateService.walletTransactions$.pipe( + startWith([]), + scan((summaries, newTransactions) => { + const newSummaries: AddressTxSummary[] = []; + for (const tx of newTransactions) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // add tx to summary + const txSummary: AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: tx.status.block_height, + time: tx.status.block_time, + }; + wallet[address].transactions?.push(txSummary); + newSummaries.push(txSummary); + } } - return summary; - }, initial) + return [...summaries, ...this.deduplicateWalletTransactions(newSummaries)]; + }, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))) )), share(), ); diff --git a/frontend/src/app/components/wallet/wallet.component.html b/frontend/src/app/components/wallet/wallet.component.html index 60cc4e264..52b7b02a5 100644 --- a/frontend/src/app/components/wallet/wallet.component.html +++ b/frontend/src/app/components/wallet/wallet.component.html @@ -5,7 +5,7 @@
- +
@@ -35,31 +35,31 @@
- + + + Confirmed balance + + + + Confirmed UTXOs + {{ walletStats.utxos }} + Total received - + - - Total sent - - - - - Balance - - +
- +
- +

Balance History

@@ -67,56 +67,16 @@
- +
+ -
-
-

- Transactions -

-
+ - - -
- - - -
-
-
-
-
-
- -
-
-
- -
-
- -
-
-
- -
- - -
- -
-
- -
- - - -
+
@@ -142,21 +102,11 @@ - +
Error loading wallet data. -
- - There many transactions in this wallet, more than your backend can handle. See more on setting up a stronger backend. -

- Consider viewing this wallet on the official Mempool website instead: -
-
- https://mempool.space/wallet?addresses={{ addressStrings.join(',') }} -
- http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/wallet?addresses={{ addressStrings.join(',') }}

({{ error | httpErrorMsg }})
@@ -172,10 +122,6 @@
- -
- -
diff --git a/frontend/src/app/components/wallet/wallet.component.ts b/frontend/src/app/components/wallet/wallet.component.ts index e91def889..be04e1760 100644 --- a/frontend/src/app/components/wallet/wallet.component.ts +++ b/frontend/src/app/components/wallet/wallet.component.ts @@ -1,15 +1,101 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { ElectrsApiService } from '../../services/electrs-api.service'; -import { switchMap, filter, catchError, map, tap, share } from 'rxjs/operators'; -import { Address, Transaction } from '../../interfaces/electrs.interface'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { switchMap, catchError, map, tap, shareReplay, startWith, scan } from 'rxjs/operators'; +import { Address, AddressTxSummary, ChainStats, Transaction } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; -import { AudioService } from '../../services/audio.service'; import { ApiService } from '../../services/api.service'; -import { of, merge, Subscription, Observable, combineLatest, forkJoin } from 'rxjs'; +import { of, Observable, Subscription } from 'rxjs'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; +import { WalletAddress } from '../../interfaces/node-api.interface'; + +class WalletStats implements ChainStats { + addresses: string[]; + funded_txo_count: number; + funded_txo_sum: number; + spent_txo_count: number; + spent_txo_sum: number; + tx_count: number; + + constructor (stats: ChainStats[], addresses: string[]) { + Object.assign(this, stats.reduce((acc, stat) => { + acc.funded_txo_count += stat.funded_txo_count; + acc.funded_txo_sum += stat.funded_txo_sum; + acc.spent_txo_count += stat.spent_txo_count; + acc.spent_txo_sum += stat.spent_txo_sum; + return acc; + }, { + funded_txo_count: 0, + funded_txo_sum: 0, + spent_txo_count: 0, + spent_txo_sum: 0, + tx_count: 0, + }) + ); + this.addresses = addresses; + } + + public addTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) { + this.spendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (this.addresses.includes(vout.scriptpubkey_address)) { + this.fundTxo(vout.value); + } + } + this.tx_count++; + } + + public removeTx(tx: Transaction): void { + for (const vin of tx.vin) { + if (this.addresses.includes(vin.prevout?.scriptpubkey_address)) { + this.unspendTxo(vin.prevout.value); + } + } + for (const vout of tx.vout) { + if (this.addresses.includes(vout.scriptpubkey_address)) { + this.unfundTxo(vout.value); + } + } + this.tx_count--; + } + + private fundTxo(value: number): void { + this.funded_txo_sum += value; + this.funded_txo_count++; + } + + private unfundTxo(value: number): void { + this.funded_txo_sum -= value; + this.funded_txo_count--; + } + + private spendTxo(value: number): void { + this.spent_txo_sum += value; + this.spent_txo_count++; + } + + private unspendTxo(value: number): void { + this.spent_txo_sum -= value; + this.spent_txo_count--; + } + + get balance(): number { + return this.funded_txo_sum - this.spent_txo_sum; + } + + get totalReceived(): number { + return this.funded_txo_sum; + } + + get utxos(): number { + return this.funded_txo_count - this.spent_txo_count; + } +} @Component({ selector: 'app-wallet', @@ -19,16 +105,16 @@ import { seoDescriptionNetwork } from '../../shared/common.utils'; export class WalletComponent implements OnInit, OnDestroy { network = ''; - addresses: Address[]; - addressStrings: string[]; - isLoadingAddress = true; - transactions: Transaction[]; - isLoadingTransactions = true; - retryLoadMore = false; + addresses: Address[] = []; + addressStrings: string[] = []; + walletName: string; + isLoadingWallet = true; + wallet$: Observable>; + walletAddresses$: Observable>; + walletSummary$: Observable; + walletStats$: Observable; error: any; - mainSubscription: Subscription; - wsSubscription: Subscription; - addressLoadingStatus$: Observable; + walletSubscription: Subscription; collapseAddresses: boolean = true; @@ -38,16 +124,10 @@ export class WalletComponent implements OnInit, OnDestroy { sent = 0; chainBalance = 0; - private tempTransactions: Transaction[]; - private timeTxIndexes: number[]; - private lastTransactionTxId: string; - constructor( private route: ActivatedRoute, - private electrsApiService: ElectrsApiService, private websocketService: WebsocketService, private stateService: StateService, - private audioService: AudioService, private apiService: ApiService, private seoService: SeoService, ) { } @@ -55,275 +135,156 @@ export class WalletComponent implements OnInit, OnDestroy { ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.websocketService.want(['blocks']); - - const addresses$ = this.route.queryParamMap.pipe( - map((queryParams) => (queryParams.get('addresses') as string)?.split(',').map(this.normalizeAddress)), - tap(addresses => { - this.addressStrings = addresses; - this.error = undefined; - this.isLoadingAddress = true; - this.fullyLoaded = false; - this.addresses = []; - this.isLoadingTransactions = true; - this.transactions = null; - document.body.scrollTo(0, 0); - const titleLabel = addresses[0] + (addresses.length > 1 ? ` +${addresses.length - 1} addresses` : ''); - this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${titleLabel}}:INTERPOLATION:`); - this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${titleLabel}:INTERPOLATION:.`); + this.wallet$ = this.route.paramMap.pipe( + map((params: ParamMap) => params.get('wallet') as string), + tap((walletName: string) => { + this.walletName = walletName; + this.websocketService.startTrackingWallet(walletName); + this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`); }), - share() + switchMap((walletName: string) => this.apiService.getWallet$(walletName).pipe( + catchError((err) => { + this.error = err; + this.seoService.logSoft404(); + console.log(err); + return of({}); + }) + )), + shareReplay(1), ); - this.addressLoadingStatus$ = addresses$ - .pipe( - switchMap(() => this.stateService.loadingIndicators$), - map((indicators) => indicators['address-' + this.addressStrings.join(',')] !== undefined ? indicators['address-' + this.addressStrings.join(',')] : 0) - ); - - this.mainSubscription = combineLatest([ - addresses$, - merge( - of(true), - this.stateService.connectionState$.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0)), - ), - ]).pipe( - switchMap(([addresses]) => { - return forkJoin( - addresses.map((address) => - address.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) - ? this.electrsApiService.getPubKeyAddress$(address) - : this.electrsApiService.getAddress$(address) - ) - ); - }), - tap((addresses: Address[]) => { - this.addresses = addresses; - this.updateChainStats(); - this.isLoadingAddress = false; - this.isLoadingTransactions = true; - this.websocketService.startTrackAddresses(addresses.map(address => address.address)); - }), - switchMap((addresses) => { - return addresses[0].is_pubkey - ? this.electrsApiService.getScriptHashesTransactions$(addresses.map(address => (address.address.length === 66 ? '21' : '41') + address.address + 'ac')) - : this.electrsApiService.getAddressesTransactions$(addresses.map(address => address.address)); - }), - switchMap((transactions) => { - this.tempTransactions = transactions; - if (transactions.length) { - this.lastTransactionTxId = transactions[transactions.length - 1].txid; - } - - const fetchTxs: string[] = []; - this.timeTxIndexes = []; - transactions.forEach((tx, index) => { - if (!tx.status.confirmed) { - fetchTxs.push(tx.txid); - this.timeTxIndexes.push(index); - } - }); - if (!fetchTxs.length) { - return of([]); - } - return this.apiService.getTransactionTimes$(fetchTxs).pipe( - catchError((err) => { - this.isLoadingAddress = false; - this.isLoadingTransactions = false; - this.error = err; - this.seoService.logSoft404(); - console.log(err); - return of([]); - }) - ); - }) - ) - .subscribe((times: number[] | null) => { - if (!times) { - return; + this.walletAddresses$ = this.wallet$.pipe( + map(wallet => { + const walletInfo: Record = {}; + for (const address of Object.keys(wallet)) { + walletInfo[address] = { + address, + chain_stats: wallet[address].stats, + mempool_stats: { + funded_txo_count: 0, + funded_txo_sum: 0, + spent_txo_count: 0, spent_txo_sum: 0, tx_count: 0 + }, + }; } - times.forEach((time, index) => { - this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time; - }); - this.tempTransactions.sort((a, b) => { - if (b.status.confirmed) { - if (b.status.block_height === a.status.block_height) { - return b.status.block_time - a.status.block_time; + return walletInfo; + }), + switchMap(initial => this.stateService.walletTransactions$.pipe( + startWith(null), + scan((wallet, walletTransactions) => { + for (const tx of (walletTransactions || [])) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } } - return b.status.block_height - a.status.block_height; - } - return b.firstSeen - a.firstSeen; - }); - - this.transactions = this.tempTransactions; - this.isLoadingTransactions = false; - }, - (error) => { - console.log(error); - this.error = error; - this.seoService.logSoft404(); - this.isLoadingAddress = false; - }); - - this.wsSubscription = this.stateService.multiAddressTransactions$.subscribe(update => { - for (const address of Object.keys(update)) { - for (const transaction of update[address].mempool) { - this.addTransaction(transaction); - } - for (const transaction of update[address].confirmed) { - const tx = this.transactions.find((t) => t.txid === transaction.txid); - if (tx) { - this.removeTransaction(tx); - tx.status = transaction.status; - this.transactions = this.transactions.slice(); - this.audioService.playSound('magic'); - } else { - if (this.addTransaction(transaction, false)) { - this.audioService.playSound('magic'); + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // update address stats + wallet[address].chain_stats.tx_count++; + wallet[address].chain_stats.funded_txo_count += fundedCount[address] || 0; + wallet[address].chain_stats.spent_txo_count += spentCount[address] || 0; + wallet[address].chain_stats.funded_txo_sum += funded[address] || 0; + wallet[address].chain_stats.spent_txo_sum += spent[address] || 0; } } - } - for (const transaction of update[address].removed) { - this.removeTransaction(transaction); - } - } - }); - } - - addTransaction(transaction: Transaction, playSound: boolean = true): boolean { - if (this.transactions.some((t) => t.txid === transaction.txid)) { - return false; - } - - this.transactions.unshift(transaction); - this.transactions = this.transactions.slice(); - this.txCount++; - - if (playSound) { - if (transaction.vout.some((vout) => this.addressStrings.includes(vout?.scriptpubkey_address))) { - this.audioService.playSound('cha-ching'); - } else { - this.audioService.playSound('chime'); - } - } - - for (const address of this.addresses) { - let match = false; - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === address.address) { - match = true; - this.sent += vin.prevout.value; - if (transaction.status?.confirmed) { - address.chain_stats.funded_txo_count++; - address.chain_stats.funded_txo_sum += vin.prevout.value; - } else { - address.mempool_stats.funded_txo_count++; - address.mempool_stats.funded_txo_sum += vin.prevout.value; - } - } - }); - transaction.vout.forEach((vout) => { - match = true; - if (vout?.scriptpubkey_address === address.address) { - this.received += vout.value; - } - if (transaction.status?.confirmed) { - address.chain_stats.spent_txo_count++; - address.chain_stats.spent_txo_sum += vout.value; - } else { - address.mempool_stats.spent_txo_count++; - address.mempool_stats.spent_txo_sum += vout.value; - } - }); - if (match) { - if (transaction.status?.confirmed) { - address.chain_stats.tx_count++; - } else { - address.mempool_stats.tx_count++; - } - } - } - - return true; - } - - removeTransaction(transaction: Transaction): boolean { - const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid)); - if (index === -1) { - return false; - } - - this.transactions.splice(index, 1); - this.transactions = this.transactions.slice(); - this.txCount--; - - for (const address of this.addresses) { - let match = false; - transaction.vin.forEach((vin) => { - if (vin?.prevout?.scriptpubkey_address === address.address) { - match = true; - this.sent -= vin.prevout.value; - if (transaction.status?.confirmed) { - address.chain_stats.funded_txo_count--; - address.chain_stats.funded_txo_sum -= vin.prevout.value; - } else { - address.mempool_stats.funded_txo_count--; - address.mempool_stats.funded_txo_sum -= vin.prevout.value; - } - } - }); - transaction.vout.forEach((vout) => { - match = true; - if (vout?.scriptpubkey_address === address.address) { - this.received -= vout.value; - } - if (transaction.status?.confirmed) { - address.chain_stats.spent_txo_count--; - address.chain_stats.spent_txo_sum -= vout.value; - } else { - address.mempool_stats.spent_txo_count--; - address.mempool_stats.spent_txo_sum -= vout.value; - } - }); - if (match) { - if (transaction.status?.confirmed) { - address.chain_stats.tx_count--; - } else { - address.mempool_stats.tx_count--; - } - } - } - - return true; - } - - loadMore(): void { - if (this.isLoadingTransactions || this.fullyLoaded) { - return; - } - this.isLoadingTransactions = true; - this.retryLoadMore = false; - - (this.addresses[0].is_pubkey - ? this.electrsApiService.getScriptHashesTransactions$(this.addresses.map(address => (address.address.length === 66 ? '21' : '41') + address.address + 'ac'), this.lastTransactionTxId) - : this.electrsApiService.getAddressesTransactions$(this.addresses.map(address => address.address), this.lastTransactionTxId) - ).pipe( - catchError((error) => { - this.isLoadingTransactions = false; - this.retryLoadMore = true; - // In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page. - if (error.status === 422) { - window.location.reload(); - } - return of([]); + return wallet; + }, initial) + )), + tap(() => { + this.isLoadingWallet = false; }) - ).subscribe((transactions: Transaction[]) => { - if (transactions && transactions.length) { - this.lastTransactionTxId = transactions[transactions.length - 1].txid; - this.transactions = this.transactions.concat(transactions); + ); + + this.walletSubscription = this.walletAddresses$.subscribe(wallet => { + this.addressStrings = Object.keys(wallet); + this.addresses = Object.values(wallet); + }); + + this.walletSummary$ = this.wallet$.pipe( + switchMap(wallet => this.stateService.walletTransactions$.pipe( + startWith([]), + scan((summaries, newTransactions) => { + const newSummaries: AddressTxSummary[] = []; + for (const tx of newTransactions) { + const funded: Record = {}; + const spent: Record = {}; + const fundedCount: Record = {}; + const spentCount: Record = {}; + for (const vin of tx.vin) { + const address = vin.prevout?.scriptpubkey_address; + if (address && wallet[address]) { + spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0); + spentCount[address] = (spentCount[address] ?? 0) + 1; + } + } + for (const vout of tx.vout) { + const address = vout.scriptpubkey_address; + if (address && wallet[address]) { + funded[address] = (funded[address] ?? 0) + (vout.value ?? 0); + fundedCount[address] = (fundedCount[address] ?? 0) + 1; + } + } + for (const address of Object.keys({ ...funded, ...spent })) { + // add tx to summary + const txSummary: AddressTxSummary = { + txid: tx.txid, + value: (funded[address] ?? 0) - (spent[address] ?? 0), + height: tx.status.block_height, + time: tx.status.block_time, + }; + wallet[address].transactions?.push(txSummary); + newSummaries.push(txSummary); + } + } + return [...summaries, ...this.deduplicateWalletTransactions(newSummaries)]; + }, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))) + )), + ); + + this.walletStats$ = this.wallet$.pipe( + switchMap(wallet => { + const walletStats = new WalletStats(Object.values(wallet).map(w => w.stats), Object.keys(wallet)); + return this.stateService.walletTransactions$.pipe( + startWith([]), + scan((stats, newTransactions) => { + for (const tx of newTransactions) { + stats.addTx(tx); + } + return stats; + }, walletStats), + ); + }), + ); + } + + deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] { + const transactions = new Map(); + for (const tx of walletTransactions) { + if (transactions.has(tx.txid)) { + transactions.get(tx.txid).value += tx.value; } else { - this.fullyLoaded = true; + transactions.set(tx.txid, tx); } - this.isLoadingTransactions = false; + } + return Array.from(transactions.values()).sort((a, b) => { + if (a.height === b.height) { + return b.tx_position - a.tx_position; + } + return b.height - a.height; }); } @@ -335,26 +296,8 @@ export class WalletComponent implements OnInit, OnDestroy { } } - updateChainStats(): void { - let received = 0; - let sent = 0; - let txCount = 0; - let chainBalance = 0; - for (const address of this.addresses) { - received += address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum; - sent += address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum; - txCount += address.chain_stats.tx_count + address.mempool_stats.tx_count; - chainBalance += (address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum); - } - this.received = received; - this.sent = sent; - this.txCount = txCount; - this.chainBalance = chainBalance; - } - ngOnDestroy(): void { - this.mainSubscription.unsubscribe(); - this.websocketService.stopTrackingAddresses(); - this.wsSubscription.unsubscribe(); + this.websocketService.stopTrackingWallet(); + this.walletSubscription.unsubscribe(); } } diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 5e7707a89..b9940fc84 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -90,7 +90,7 @@ const routes: Routes = [ } }, { - path: 'wallet', + path: 'wallet/:wallet', children: [], component: WalletComponent, data: { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 315ba9b20..0091262e1 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -1,4 +1,4 @@ -import { AddressTxSummary, Block, Transaction } from "./electrs.interface"; +import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface"; export interface OptimizedMempoolStats { added: number; @@ -474,5 +474,6 @@ export interface TxResult { export interface WalletAddress { address: string; active: boolean; - transactions?: AddressTxSummary[]; + stats: ChainStats; + transactions: AddressTxSummary[]; } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 059f3d45c..5e4075a52 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -159,7 +159,7 @@ export class StateService { mempoolRemovedTransactions$ = new Subject(); multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>(); blockTransactions$ = new Subject(); - walletTransactions$ = new Subject>(); + walletTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1);
Confidential