Refactor address page component with AddressStats class

This commit is contained in:
Mononaut 2024-06-10 22:02:14 +00:00
parent 684ad9f0e6
commit 746a045c48
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
2 changed files with 92 additions and 41 deletions

View File

@ -28,16 +28,16 @@
<ng-template [ngIf]="!address.electrum">
<tr>
<td i18n="address.total-received">Total received</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received" [noFiat]="true"></app-amount></td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.funded_txo_sum + mempoolStats.funded_txo_sum" [noFiat]="true"></app-amount></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="sent" [noFiat]="true"></app-amount></td>
<td *ngIf="address.chain_stats.spent_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.spent_txo_sum + mempoolStats.spent_txo_sum" [noFiat]="true"></app-amount></td>
</tr>
</ng-template>
<tr>
<td i18n="address.balance">Balance</td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="received - sent" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="received - sent"></app-fiat></span></td>
<td *ngIf="address.chain_stats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.balance + mempoolStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance + mempoolStats.balance"></app-fiat></span></td>
</tr>
</tbody>
</table>
@ -76,8 +76,8 @@
<div class="title-tx">
<h2 class="text-left">
<ng-template [ngIf]="!transactions?.length">&nbsp;</ng-template>
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transaction</ng-template>
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions</ng-template>
<ng-template i18n="X of X Address Transaction" [ngIf]="transactions?.length === 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transaction</ng-template>
<ng-template i18n="X of X Address Transactions (Plural)" [ngIf]="transactions?.length > 1">{{ (transactions?.length | number) || '?' }} of {{ mempoolStats.tx_count + chainStats.tx_count | number }} transactions</ng-template>
</h2>
</div>

View File

@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
import { Address, ChainStats, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
@ -12,6 +12,78 @@ import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
class AddressStats implements ChainStats {
address: string;
scriptpubkey?: string;
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
constructor (stats: ChainStats, address: string, scriptpubkey?: string) {
Object.assign(this, stats);
this.address = address;
this.scriptpubkey = scriptpubkey;
}
public addTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
this.spendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
this.fundTxo(vout.value);
}
}
this.tx_count++;
}
public removeTx(tx: Transaction): void {
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_address === this.address || (this.scriptpubkey === vin.prevout?.scriptpubkey)) {
this.unspendTxo(vin.prevout.value);
}
}
for (const vout of tx.vout) {
if (vout.scriptpubkey_address === this.address || (this.scriptpubkey === vout.scriptpubkey)) {
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 utxos(): number {
return this.funded_txo_count - this.spent_txo_count;
}
}
@Component({
selector: 'app-address',
templateUrl: './address.component.html',
@ -35,9 +107,9 @@ export class AddressComponent implements OnInit, OnDestroy {
addressInfo: null | AddressInformation = null;
fullyLoaded = false;
txCount = 0;
received = 0;
sent = 0;
chainStats: AddressStats;
mempoolStats: AddressStats;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
@ -55,7 +127,7 @@ export class AddressComponent implements OnInit, OnDestroy {
private seoService: SeoService,
) { }
ngOnInit() {
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.websocketService.want(['blocks']);
@ -175,7 +247,7 @@ export class AddressComponent implements OnInit, OnDestroy {
});
this.transactions = this.tempTransactions;
if (this.transactions.length === this.txCount) {
if (this.transactions.length === (this.mempoolStats.tx_count + this.chainStats.tx_count)) {
this.fullyLoaded = true;
}
this.isLoadingTransactions = false;
@ -196,11 +268,13 @@ export class AddressComponent implements OnInit, OnDestroy {
this.mempoolTxSubscription = this.stateService.mempoolTransactions$
.subscribe(tx => {
this.addTransaction(tx);
this.mempoolStats.addTx(tx);
});
this.mempoolRemovedTxSubscription = this.stateService.mempoolRemovedTransactions$
.subscribe(tx => {
this.removeTransaction(tx);
this.mempoolStats.removeTx(tx);
});
this.blockTxSubscription = this.stateService.blockTransactions$
@ -209,12 +283,14 @@ export class AddressComponent implements OnInit, OnDestroy {
if (tx) {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.mempoolStats.removeTx(transaction);
this.audioService.playSound('magic');
} else {
if (this.addTransaction(transaction, false)) {
this.audioService.playSound('magic');
}
}
this.chainStats.addTx(transaction);
});
}
@ -225,7 +301,6 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.unshift(transaction);
this.transactions = this.transactions.slice();
this.txCount++;
if (playSound) {
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
@ -235,17 +310,6 @@ export class AddressComponent implements OnInit, OnDestroy {
}
}
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received += vout.value;
}
});
return true;
}
@ -257,23 +321,11 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions.splice(index, 1);
this.transactions = this.transactions.slice();
this.txCount--;
transaction.vin.forEach((vin) => {
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
this.sent -= vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout?.scriptpubkey_address === this.address.address) {
this.received -= vout.value;
}
});
return true;
}
loadMore() {
loadMore(): void {
if (this.isLoadingTransactions || this.fullyLoaded) {
return;
}
@ -301,10 +353,9 @@ export class AddressComponent implements OnInit, OnDestroy {
});
}
updateChainStats() {
this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
updateChainStats(): void {
this.chainStats = new AddressStats(this.address.chain_stats, this.address.address);
this.mempoolStats = new AddressStats(this.address.mempool_stats, this.address.address);
}
setBalancePeriod(period: 'all' | '1m'): boolean {
@ -319,7 +370,7 @@ export class AddressComponent implements OnInit, OnDestroy {
);
}
ngOnDestroy() {
ngOnDestroy(): void {
this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe();
this.mempoolRemovedTxSubscription.unsubscribe();