Merge pull request #5151 from mempool/mononaut/address-redesign-phase-1
Address page redesign phase 1
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">
|
||||
<span
|
||||
*ngIf="label"
|
||||
class="badge badge-pill badge-warning"
|
||||
class="badge badge-pill badge-warning {{ class }}"
|
||||
>{{ label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -15,6 +15,6 @@
|
||||
<ng-template #default>
|
||||
<span
|
||||
*ngIf="label"
|
||||
class="badge badge-pill badge-warning"
|
||||
class="badge badge-pill badge-warning {{ class }}"
|
||||
>{{ label }}</span>
|
||||
</ng-template>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { parseMultisigScript } from '../../bitcoin.utils';
|
||||
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-labels',
|
||||
@@ -12,9 +12,11 @@ import { parseMultisigScript } from '../../bitcoin.utils';
|
||||
export class AddressLabelsComponent implements OnChanges {
|
||||
network = '';
|
||||
|
||||
@Input() address: AddressTypeInfo;
|
||||
@Input() vin: Vin;
|
||||
@Input() vout: Vout;
|
||||
@Input() channel: any;
|
||||
@Input() class: string = '';
|
||||
|
||||
label?: string;
|
||||
|
||||
@@ -27,10 +29,10 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
if (this.channel) {
|
||||
this.handleChannel();
|
||||
} else if (this.address) {
|
||||
this.handleAddress();
|
||||
} else if (this.vin) {
|
||||
this.handleVin();
|
||||
} else if (this.vout) {
|
||||
this.handleVout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,74 +43,22 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`;
|
||||
}
|
||||
|
||||
handleAddress() {
|
||||
if (this.address?.scripts.size) {
|
||||
const script = this.address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
this.label = script.template.label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleVin() {
|
||||
if (this.vin.inner_witnessscript_asm) {
|
||||
if (this.vin.inner_witnessscript_asm.indexOf('OP_DEPTH OP_PUSHNUM_12 OP_EQUAL OP_IF OP_PUSHNUM_11') === 0 || this.vin.inner_witnessscript_asm.indexOf('OP_PUSHNUM_15 OP_CHECKMULTISIG OP_IFDUP OP_NOTIF OP_PUSHBYTES_2') === 1259) {
|
||||
if (this.vin.witness.length > 11) {
|
||||
this.label = 'Liquid Peg Out';
|
||||
} else {
|
||||
this.label = 'Emergency Liquid Peg Out';
|
||||
}
|
||||
return;
|
||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
||||
if (address?.scripts.size) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
this.label = script.template.label;
|
||||
}
|
||||
|
||||
const topElement = this.vin.witness[this.vin.witness.length - 2];
|
||||
if (/^OP_IF OP_PUSHBYTES_33 \w{66} OP_ELSE OP_PUSH(NUM_\d+|BYTES_(1 \w{2}|2 \w{4})) OP_CSV OP_DROP OP_PUSHBYTES_33 \w{66} OP_ENDIF OP_CHECKSIG$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#commitment-transaction-outputs
|
||||
if (topElement === '01') {
|
||||
// top element is '01' to get in the revocation path
|
||||
this.label = 'Revoked Lightning Force Close';
|
||||
} else {
|
||||
// top element is '', this is a delayed to_local output
|
||||
this.label = 'Lightning Force Close';
|
||||
}
|
||||
return;
|
||||
} else if (
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_NOTIF OP_DROP OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm) ||
|
||||
/^OP_DUP OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUAL OP_IF OP_CHECKSIG OP_ELSE OP_PUSHBYTES_33 \w{66} OP_SWAP OP_SIZE OP_PUSHBYTES_1 20 OP_EQUAL OP_IF OP_HASH160 OP_PUSHBYTES_20 \w{40} OP_EQUALVERIFY OP_PUSHNUM_2 OP_SWAP OP_PUSHBYTES_33 \w{66} OP_PUSHNUM_2 OP_CHECKMULTISIG OP_ELSE OP_DROP OP_PUSHBYTES_3 \w{6} OP_CLTV OP_DROP OP_CHECKSIG OP_ENDIF (OP_PUSHNUM_1 OP_CSV OP_DROP |)OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)
|
||||
) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#offered-htlc-outputs
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#received-htlc-outputs
|
||||
if (topElement.length === 66) {
|
||||
// top element is a public key
|
||||
this.label = 'Revoked Lightning HTLC';
|
||||
} else if (topElement) {
|
||||
// top element is a preimage
|
||||
this.label = 'Lightning HTLC';
|
||||
} else {
|
||||
// top element is '' to get in the expiry of the script
|
||||
this.label = 'Expired Lightning HTLC';
|
||||
}
|
||||
return;
|
||||
} else if (/^OP_PUSHBYTES_33 \w{66} OP_CHECKSIG OP_IFDUP OP_NOTIF OP_PUSHNUM_16 OP_CSV OP_ENDIF$/.test(this.vin.inner_witnessscript_asm)) {
|
||||
// https://github.com/lightning/bolts/blob/master/03-transactions.md#to_local_anchor-and-to_remote_anchor-output-option_anchors
|
||||
if (topElement) {
|
||||
// top element is a signature
|
||||
this.label = 'Lightning Anchor';
|
||||
} else {
|
||||
// top element is '', it has been swept after 16 blocks
|
||||
this.label = 'Swept Lightning Anchor';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.detectMultisig(this.vin.inner_witnessscript_asm);
|
||||
}
|
||||
|
||||
this.detectMultisig(this.vin.inner_redeemscript_asm);
|
||||
|
||||
this.detectMultisig(this.vin.prevout.scriptpubkey_asm);
|
||||
}
|
||||
|
||||
detectMultisig(script: string) {
|
||||
const ms = parseMultisigScript(script);
|
||||
|
||||
if (ms) {
|
||||
this.label = $localize`:@@address-label.multisig:Multisig ${ms.m}:multisigM: of ${ms.n}:multisigN:`;
|
||||
}
|
||||
}
|
||||
|
||||
handleVout() {
|
||||
this.detectMultisig(this.vout.scriptpubkey_asm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
<h1 i18n="shared.address">Address</h1>
|
||||
<div class="tx-link">
|
||||
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||
<app-clipboard [text]="addressString"></app-clipboard>
|
||||
<app-clipboard [text]="addressString" [size]="isMobile ? 'large' : 'normal'"></app-clipboard>
|
||||
<span style="position: relative; cursor: pointer" (mouseover)="showQR = true" (mouseout)="showQR = false" (pointerdown)="showQR = true">
|
||||
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true" [style.font-size]="isMobile ? '18px' : '12px'"></fa-icon>
|
||||
<div class="qr-wrapper" [hidden]="!showQR">
|
||||
<app-qrcode [size]="200" [data]="addressString"></app-qrcode>
|
||||
</div>
|
||||
</span>
|
||||
</app-truncate>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,40 +20,47 @@
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped address-table">
|
||||
<tbody>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||
</app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="w-100 d-block d-md-none"></div>
|
||||
<div class="col-md qrcode-col">
|
||||
<div class="qr-wrapper">
|
||||
<app-qrcode [data]="address.address"></app-qrcode>
|
||||
@if (isMobile) {
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="balanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
|
||||
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped table-fixed address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="balanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="utxoRow"></ng-container>
|
||||
@if (network === 'liquid' || network === 'liquidtestnet') {
|
||||
<ng-container *ngTemplateOutlet="liquidRow"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="volumeRow"></ng-container>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped table-fixed address-table">
|
||||
<tbody>
|
||||
<ng-container *ngTemplateOutlet="pendingBalanceRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="pendingUtxoRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="typeRow"></ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,8 +89,8 @@
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left">
|
||||
<ng-template [ngIf]="!transactions?.length"> </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>
|
||||
|
||||
@@ -182,3 +195,57 @@
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
<ng-template #balanceRow>
|
||||
<tr>
|
||||
<td i18n="address.balance">Balance</td>
|
||||
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.balance" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="chainStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingBalanceRow>
|
||||
<tr>
|
||||
<td i18n="address.unconfirmed-balance" class="font-italic">unconfirmed balance</td>
|
||||
<td *ngIf="mempoolStats.funded_txo_sum !== undefined; else confidentialTd" class="font-italic"><app-amount [satoshis]="mempoolStats.balance" [noFiat]="true" [addPlus]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolStats.balance"></app-fiat></span></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #utxoRow>
|
||||
<tr>
|
||||
<td i18n="address.utxos" i18n-ngbTooltip="unspent-transaction-outputs" ngbTooltip="unspent transaction outputs">UTXOs</td>
|
||||
<td>{{ chainStats.utxos }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pendingUtxoRow>
|
||||
<tr>
|
||||
<td i18n="address.unconfirmed-utxos" class="font-italic">unconfirmed UTXOs</td>
|
||||
<td class="font-italic">{{ mempoolStats.utxos > 0 ? '+' : ''}}{{ mempoolStats.utxos }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #volumeRow>
|
||||
<tr>
|
||||
<td i18n="address.volume">Volume</td>
|
||||
<td *ngIf="chainStats.funded_txo_sum !== undefined; else confidentialTd"><app-amount [satoshis]="chainStats.volume + mempoolStats.volume"></app-amount></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #typeRow>
|
||||
<tr>
|
||||
<td i18n="address.type">Type</td>
|
||||
<td class="wrap-cell"><app-address-type [address]="addressTypeInfo"></app-address-type><app-address-labels [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels></td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #liquidRow>
|
||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||
<td i18n="address.unconfidential">Unconfidential</td>
|
||||
<td>
|
||||
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [textAlign]="isMobile ? 'end' : 'start'" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||
</app-truncate>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
@@ -1,16 +1,14 @@
|
||||
.qr-wrapper {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0px;
|
||||
border: solid 10px var(--active-bg);
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
@media (min-width: 992px){
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
display: block;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.fiat {
|
||||
@@ -25,10 +23,14 @@
|
||||
tr td {
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
@media (min-width: 768px) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
&.wrap-cell {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +80,10 @@ h1 {
|
||||
top: 9px;
|
||||
position: relative;
|
||||
@media (min-width: 576px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 11px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 17px;
|
||||
}
|
||||
}
|
||||
@@ -96,17 +98,6 @@ h1 {
|
||||
.liquid-address {
|
||||
.address-table {
|
||||
table-layout: fixed;
|
||||
|
||||
tr td:first-child {
|
||||
width: 170px;
|
||||
}
|
||||
tr td:last-child {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.qrcode-col {
|
||||
flex-grow: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, HostListener } 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, Vin } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
@@ -11,6 +11,83 @@ import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { AddressTypeInfo } from '../../shared/address-utils';
|
||||
|
||||
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 volume(): 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',
|
||||
@@ -20,6 +97,9 @@ import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
export class AddressComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
|
||||
isMobile: boolean;
|
||||
showQR: boolean = false;
|
||||
|
||||
address: Address;
|
||||
addressString: string;
|
||||
isLoadingAddress = true;
|
||||
@@ -33,11 +113,14 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
blockTxSubscription: Subscription;
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
addressInfo: null | AddressInformation = null;
|
||||
addressTypeInfo: null | AddressTypeInfo;
|
||||
|
||||
fullyLoaded = false;
|
||||
txCount = 0;
|
||||
received = 0;
|
||||
sent = 0;
|
||||
chainStats: AddressStats;
|
||||
mempoolStats: AddressStats;
|
||||
|
||||
exampleChannel?: any;
|
||||
|
||||
now = Date.now() / 1000;
|
||||
balancePeriod: 'all' | '1m' = 'all';
|
||||
|
||||
@@ -55,10 +138,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.onResize();
|
||||
|
||||
this.addressLoadingStatus$ = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.loadingIndicators$),
|
||||
@@ -75,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.addressInfo = null;
|
||||
this.exampleChannel = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
@@ -83,6 +169,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}: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 ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
this.addressTypeInfo = new AddressTypeInfo(this.stateService.network || 'mainnet', this.addressString);
|
||||
|
||||
return merge(
|
||||
of(true),
|
||||
this.stateService.connectionState$
|
||||
@@ -175,11 +263,19 @@ 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;
|
||||
|
||||
let addressVin: Vin[] = [];
|
||||
for (const tx of this.transactions) {
|
||||
addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address));
|
||||
}
|
||||
this.addressTypeInfo.processInputs(addressVin);
|
||||
// hack to trigger change detection
|
||||
this.addressTypeInfo = this.addressTypeInfo.clone();
|
||||
|
||||
if (!this.showBalancePeriod()) {
|
||||
this.setBalancePeriod('all');
|
||||
} else {
|
||||
@@ -196,11 +292,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 +307,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 +325,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 +334,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 +345,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 +377,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 +394,12 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth < 768;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.mainSubscription.unsubscribe();
|
||||
this.mempoolTxSubscription.unsubscribe();
|
||||
this.mempoolRemovedTxSubscription.unsubscribe();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
|
||||
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
|
||||
<app-svg-images name="clippy" [width]="size === 'small' ? '10' : '13'" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -9,7 +9,7 @@
|
||||
<ng-template #btnLink>
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
|
||||
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
|
||||
<app-svg-images name="clippy" [width]="size === 'small' ? '10' : '13'" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</button>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
@@ -13,11 +13,17 @@ export class ClipboardComponent implements AfterViewInit {
|
||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
||||
@Input() button = false;
|
||||
@Input() class = 'btn btn-secondary ml-1';
|
||||
@Input() size: 'small' | 'normal' = 'normal';
|
||||
@Input() size: 'small' | 'normal' | 'large' = 'normal';
|
||||
@Input() text: string;
|
||||
@Input() leftPadding = true;
|
||||
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
||||
|
||||
widths = {
|
||||
small: '10',
|
||||
normal: '13',
|
||||
large: '18',
|
||||
};
|
||||
|
||||
clipboard: any;
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
<app-address-labels [vin]="vin" [channel]="tx._channels && tx._channels.inputs[vindex] ? tx._channels.inputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
Reference in New Issue
Block a user