Merge branch 'master' into nymkappa/charts-landscape-mobile
This commit is contained in:
@@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = null;
|
||||
this.addressInfo = null;
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
|
||||
return this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
return (this.addressString.match(/[a-f0-9]{130}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
|
||||
@@ -81,6 +81,7 @@ h1 {
|
||||
top: 11px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addressInfo = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
@@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
switchMap(() => (
|
||||
this.addressString.match(/[a-f0-9]{130}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
@@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||
return address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
this.tempTransactions = transactions;
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'fullrbf':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -339,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isFullRbf = {};
|
||||
const isRbf = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -363,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isFullRbf[txid] = true;
|
||||
isRbf[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
@@ -381,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
@@ -398,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
|
||||
@@ -117,7 +117,14 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
this.stateService.isLoadingMempool$
|
||||
]).pipe(
|
||||
switchMap(([loadingBlocks, loadingMempool]) => {
|
||||
return of(loadingBlocks || loadingMempool);
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocks$ = merge(
|
||||
of(true),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining-dashboard',
|
||||
@@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./mining-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit {
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private router: Router
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||
}
|
||||
@@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
this.router.events.subscribe((e: NavigationStart) => {
|
||||
if (e.type === EventType.NavigationStart) {
|
||||
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private router: Router,
|
||||
@@ -55,11 +57,26 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
this.stateService.searchFocus$.subscribe(() => {
|
||||
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
|
||||
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
|
||||
} else if (this.searchInput) {
|
||||
this.searchInput.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: ['', Validators.required],
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, 132) === this.address))
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
@@ -56,7 +56,9 @@
|
||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||
<span>P2PK</span>
|
||||
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, 132)]" title="{{ vin.prevout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||
</a></span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||
@@ -182,12 +184,19 @@
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, 132) === this.address))
|
||||
}">
|
||||
<td class="address-cell">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<ng-template #pubkey_type>
|
||||
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, 132)]" title="{{ vout.scriptpubkey.slice(2, 132) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,15 @@ h2 {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.p2pk-address {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
max-width: 100px;
|
||||
@media (min-width: 576px) {
|
||||
max-width: 200px
|
||||
}
|
||||
}
|
||||
|
||||
.grey-info-text {
|
||||
color:#6c757d;
|
||||
font-style: italic;
|
||||
|
||||
Reference in New Issue
Block a user