Merge branch 'master' into natsoni/address-history-chart-usd

This commit is contained in:
natsoni
2024-06-22 14:36:40 +09:00
62 changed files with 1228 additions and 493 deletions

View File

@@ -189,7 +189,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -201,7 +201,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -214,7 +214,7 @@
<div class="wrapper">
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-container>
</div>
@@ -355,7 +355,7 @@
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -369,7 +369,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -381,7 +381,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>

View File

@@ -10,9 +10,6 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;

View File

@@ -96,10 +96,16 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
share(),
);
this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => {
return accelerations.filter(acc => ['completed_provisional', 'completed'].includes(acc.status));
})
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap(() => {
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe(
catchError(() => {
return of([]);
}),
);
}),
share(),
);
this.blocks$ = combineLatest([

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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">&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>
@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -411,7 +411,7 @@
<td class="text-wrap">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span *ngIf="oobFees" class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band">
+<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true"></app-amount>
<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true" [addPlus]="true"></app-amount>
</span>
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%

View File

@@ -8,7 +8,7 @@ import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
@@ -44,6 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy {
latestBlocks: BlockExtended[] = [];
oobFees: number = 0;
strippedTransactions: TransactionStripped[];
accelerations: Acceleration[];
overviewTransitionDirection: string;
isLoadingOverview = true;
error: any;
@@ -68,6 +69,7 @@ export class BlockComponent implements OnInit, OnDestroy {
mode: 'projected' | 'actual' = 'projected';
overviewSubscription: Subscription;
accelerationsSubscription: Subscription;
keyNavigationSubscription: Subscription;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
@@ -183,6 +185,9 @@ export class BlockComponent implements OnInit, OnDestroy {
} else {
this.isLoadingBlock = true;
this.isLoadingOverview = true;
this.strippedTransactions = undefined;
this.blockAudit = undefined;
this.accelerations = undefined;
let blockInCache: BlockExtended;
if (isBlockHeight) {
@@ -294,158 +299,36 @@ export class BlockComponent implements OnInit, OnDestroy {
this.overviewError = err;
return of(null);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
: of([])
)
]);
})
)
.subscribe(([transactions, blockAudit, accelerations]) => {
.subscribe(([transactions, blockAudit]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
this.blockAudit = blockAudit;
const acceleratedInBlock = {};
for (const acc of accelerations) {
if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id || pool?.['pool_unique_id'] === this.block?.extras?.pool.id)) {
acceleratedInBlock[acc.txid] = acc;
}
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
const acceleration = acceleratedInBlock[tx.txid];
const boostCost = acceleration.boostCost || acceleration.bidBoost;
const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
if (acceleratedFeeRate > tx.rate) {
tx.rate = acceleratedFeeRate;
}
} else {
tx.acc = false;
}
}
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isPrioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
const isRbf = {};
const isAccelerated = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
isAccelerated[tx.txid] = true;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs || []) {
isPrioritized[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
isRbf[txid] = true;
}
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
if (isFresh[tx.txid]) {
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
tx.status = 'freshcpfp';
} else {
tx.status = 'fresh';
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'missing';
}
isMissing[tx.txid] = true;
this.numMissing++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const [index, tx] of transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0;
this.blockAudit = blockAudit;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
}
} else {
this.setAuditAvailable(false);
}
this.setupBlockAudit();
this.isLoadingOverview = false;
this.setupBlockGraphs();
this.cd.markForCheck();
});
this.accelerationsSubscription = this.block$.pipe(
switchMap((block) => {
return this.stateService.env.ACCELERATOR === true && block.height > 819500
? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height })
.pipe(catchError(() => {
return of([]);
}))
: of([]);
})
).subscribe((accelerations) => {
this.accelerations = accelerations;
if (accelerations.length) {
this.setupBlockAudit();
}
});
this.oobSubscription = this.block$.pipe(
@@ -609,6 +492,147 @@ export class BlockComponent implements OnInit, OnDestroy {
}
}
setupBlockAudit(): void {
const transactions = this.strippedTransactions || [];
const blockAudit = this.blockAudit;
const accelerations = this.accelerations || [];
const acceleratedInBlock = {};
for (const acc of accelerations) {
if (acc.pools?.some(pool => pool === this.block?.extras?.pool.id)) {
acceleratedInBlock[acc.txid] = acc;
}
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
const acceleration = acceleratedInBlock[tx.txid];
const boostCost = acceleration.boostCost || acceleration.bidBoost;
const acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize;
if (acceleratedFeeRate > tx.rate) {
tx.rate = acceleratedFeeRate;
}
} else {
tx.acc = false;
}
}
if (transactions && blockAudit) {
const inTemplate = {};
const inBlock = {};
const isAdded = {};
const isPrioritized = {};
const isCensored = {};
const isMissing = {};
const isSelected = {};
const isFresh = {};
const isSigop = {};
const isRbf = {};
const isAccelerated = {};
this.numMissing = 0;
this.numUnexpected = 0;
if (blockAudit?.template) {
for (const tx of blockAudit.template) {
inTemplate[tx.txid] = true;
if (tx.acc) {
isAccelerated[tx.txid] = true;
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
for (const txid of blockAudit.addedTxs) {
isAdded[txid] = true;
}
for (const txid of blockAudit.prioritizedTxs || []) {
isPrioritized[txid] = true;
}
for (const txid of blockAudit.missingTxs) {
isCensored[txid] = true;
}
for (const txid of blockAudit.freshTxs || []) {
isFresh[txid] = true;
}
for (const txid of blockAudit.sigopTxs || []) {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
isRbf[txid] = true;
}
for (const txid of blockAudit.acceleratedTxs || []) {
isAccelerated[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
tx.context = 'projected';
if (isCensored[tx.txid]) {
tx.status = 'censored';
} else if (inBlock[tx.txid]) {
tx.status = 'found';
} else {
if (isFresh[tx.txid]) {
if (tx.rate - (tx.fee / tx.vsize) >= 0.1) {
tx.status = 'freshcpfp';
} else {
tx.status = 'fresh';
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'missing';
}
isMissing[tx.txid] = true;
this.numMissing++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const [index, tx] of transactions.entries()) {
tx.context = 'actual';
if (index === 0) {
tx.status = null;
} else if (isAdded[tx.txid]) {
tx.status = 'added';
} else if (isPrioritized[tx.txid]) {
tx.status = 'prioritized';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
} else if (isRbf[tx.txid]) {
tx.status = 'rbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
this.numUnexpected++;
}
if (isAccelerated[tx.txid]) {
tx.status = 'accelerated';
}
}
for (const tx of transactions) {
inBlock[tx.txid] = true;
}
blockAudit.feeDelta = blockAudit.expectedFees > 0 ? (blockAudit.expectedFees - (this.block?.extras.totalFees + this.oobFees)) / blockAudit.expectedFees : 0;
blockAudit.weightDelta = blockAudit.expectedWeight > 0 ? (blockAudit.expectedWeight - this.block?.weight) / blockAudit.expectedWeight : 0;
blockAudit.txDelta = blockAudit.template.length > 0 ? (blockAudit.template.length - this.block?.tx_count) / blockAudit.template.length : 0;
this.blockAudit = blockAudit;
this.setAuditAvailable(true);
} else {
this.setAuditAvailable(false);
}
} else {
this.setAuditAvailable(false);
}
this.setupBlockGraphs();
this.cd.markForCheck();
}
setupBlockGraphs(): void {
if (this.blockAudit || this.strippedTransactions) {
this.blockGraphProjected.forEach(graph => {

View File

@@ -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>

View File

@@ -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() { }

View File

@@ -62,12 +62,12 @@
}
</a>
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED">
<div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="isDropdownVisible">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a>
<a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a>

View File

@@ -31,6 +31,7 @@ export class MasterPageComponent implements OnInit, OnDestroy {
user: any = undefined;
servicesEnabled = false;
menuOpen = false;
isDropdownVisible: boolean;
enterpriseInfo: any;
enterpriseInfo$: Subscription;
@@ -74,19 +75,27 @@ export class MasterPageComponent implements OnInit, OnDestroy {
const isServicesPage = this.router.url.includes('/services/');
this.menuOpen = isServicesPage && !this.isSmallScreen();
this.setDropdownVisibility();
}
ngOnDestroy() {
if (this.enterpriseInfo$) {
this.enterpriseInfo$.unsubscribe();
}
setDropdownVisibility(): void {
const networks = [
this.env.TESTNET_ENABLED,
this.env.TESTNET4_ENABLED,
this.env.SIGNET_ENABLED,
this.env.LIQUID_ENABLED,
this.env.LIQUID_TESTNET_ENABLED,
this.env.MAINNET_ENABLED,
];
const enabledNetworksCount = networks.filter((networkEnabled) => networkEnabled).length;
this.isDropdownVisible = enabledNetworksCount > 1;
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
isSmallScreen() {
isSmallScreen(): boolean {
return window.innerWidth <= 767.98;
}
@@ -117,4 +126,11 @@ export class MasterPageComponent implements OnInit, OnDestroy {
menuToggled(isOpen: boolean): void {
this.menuOpen = isOpen;
}
ngOnDestroy(): void {
if (this.enterpriseInfo$) {
this.enterpriseInfo$.unsubscribe();
}
}
}

View File

@@ -9,13 +9,13 @@
<ng-container *ngIf="(hosts$ | async) as hosts">
<div class="status-panel">
<table class="status-table table table-fixed table-borderless table-striped" *ngIf="(tip$ | async) as tip">
<table class="status-table table table-borderless table-striped" *ngIf="(tip$ | async) as tip">
<tbody>
<tr>
<th class="rank"></th>
<th class="flag"></th>
<th class="host">Host</th>
<th class="updated">Last checked</th>
<th class="updated">Updated</th>
<th class="rtt only-small">RTT</th>
<th class="rtt only-large">RTT</th>
<th class="height">Height</th>

View File

@@ -20,26 +20,21 @@
td, th {
padding: 0.25em;
width: 0%;
&.rank, &.flag {
width: 28px;
text-align: right;
}
&.updated {
display: none;
width: 130px;
text-align: right;
white-space: pre-wrap;
white-space: nowrap;
}
&.rtt, &.height {
width: 92px;
text-align: right;
}
&.only-small {
display: table-cell;
&.rtt {
width: 60px;
}
}
&.only-large {
display: none;
@@ -48,21 +43,17 @@
padding-right: 0.5em;
}
&.host {
width: auto;
width: 100%;
max-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (min-width: 576px) {
&.rank, &.flag {
width: 32px;
}
&.updated {
display: table-cell;
}
&.rtt, &.height {
width: 96px;
}
&.only-small {
display: none;
}

View File

@@ -62,7 +62,7 @@ export class ServerHealthComponent implements OnInit {
getLastUpdateSeconds(host: HealthCheckHost): string {
if (host.lastChecked) {
const seconds = Math.ceil((this.now - host.lastChecked) / 1000);
return `${seconds} second${seconds > 1 ? 's' : ' '} ago`;
return `${seconds} s`;
} else {
return '~';
}

View File

@@ -665,7 +665,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
}
setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
}
dismissAccelAlert(): void {

View File

@@ -726,7 +726,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
if (this.isAcceleration && initialState) {
this.showAccelerationSummary = false;
}

View File

@@ -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>