Merge branch 'master' into nymkappa/mega-branch

This commit is contained in:
nymkappa
2024-01-15 10:01:38 +01:00
80 changed files with 2793 additions and 1040 deletions

View File

@@ -422,7 +422,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, Mempool Goggles&trade;, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@@ -5,6 +5,7 @@ import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
import { ServicesApiServices } from '../../services/services-api.service';
import { AudioService } from '../../services/audio.service';
export type AccelerationEstimate = {
txSummary: TxSummary;
@@ -64,6 +65,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
constructor(
private servicesApiService: ServicesApiServices,
private storageService: StorageService,
private audioService: AudioService,
private cd: ChangeDetectorRef
) { }
@@ -187,6 +189,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.userBid
).subscribe({
next: () => {
this.audioService.playSound('ascend-chime-cartoon');
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
@@ -211,4 +214,4 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}
}

View File

@@ -1,10 +1,10 @@
<div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget">
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
<div class="clearfix"></div>
<div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations">
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>

View File

@@ -14,11 +14,24 @@
.container-xl.legacy {
max-width: 1140px;
}
.container-xl.widget-container {
min-height: 335px;
@media (max-width: 767px) {
min-height: auto;
}
}
.container {
max-width: 100%;
}
.acceleration-list {
min-height: 295px;
@media (max-width: 767px) {
min-height: auto;
}
}
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
@@ -51,34 +64,63 @@ tr, td, th {
.txid {
width: 25%;
@media (max-width: 1100px) {
padding-right: 10px;
}
@media (max-width: 875px) {
display: none;
}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
}
.fee {
width: 35%;
}
.block {
.fee-rate {
width: 20%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
@media (max-width: 840px) and (min-width: 768px) {
display: none;
}
@media (max-width: 410px) {
display: none;
}
}
.bid {
width: 30%;
min-width: 150px;
@media (max-width: 840px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 410px) {
text-align: start !important;
}
}
.time {
width: 25%;
}
.fee {
width: 35%;
@media (max-width: 1060px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
}
.block {
width: 20%;
}
.status {
width: 20%
}
@@ -122,4 +164,7 @@ tr, td, th {
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: 767px) {
height: 100px;
}
}

View File

@@ -139,6 +139,9 @@
}
.list-card {
height: 410px;
@media (max-width: 767px) {
height: auto;
}
}
.mempool-block-wrapper {

View File

@@ -1,7 +1,11 @@
<div class="block-filters" [class.filters-active]="activeFilters.length > 0" [class.menu-open]="menuOpen" [class.small]="cssWidth < 500" [class.vsmall]="cssWidth < 400" [class.tiny]="cssWidth < 200">
<a *ngIf="menuOpen" [routerLink]="['/docs/faq' | relativeUrl]" fragment="how-do-mempool-goggles-work" class="info-badges" i18n-ngbTooltip="Mempool Goggles tooltip" ngbTooltip="select filter categories to highlight matching transactions">
<span class="badge badge-pill badge-warning beta" i18n="beta">beta</span>
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" size="lg"></fa-icon>
</a>
<div class="filter-bar">
<button class="menu-toggle" (click)="menuOpen = !menuOpen">
<fa-icon [icon]="['fas', 'filter']"></fa-icon>
<button class="menu-toggle" (click)="menuOpen = !menuOpen" title="Mempool Goggles">
<app-svg-images name="goggles" width="100%" height="100%"></app-svg-images>
</button>
<div class="active-tags">
<ng-container *ngFor="let filter of activeFilters;">

View File

@@ -20,7 +20,21 @@
margin-left: 0.5em;
}
.info-badges {
display: flex;
flex-direction: row;
align-items: center;
float: right;
&:hover, &:active {
text-decoration: none;
}
}
.menu-toggle {
width: 2em;
height: 2em;
padding: 0px 1px;
opacity: 0;
cursor: pointer;
color: white;

View File

@@ -121,6 +121,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene.setColorFunction(this.overrideColors);
}
this.start();
}
ngOnDestroy(): void {

View File

@@ -11,7 +11,7 @@ export default class BlockScene {
getColor: ((tx: TxView) => Color) = defaultColorFunction;
orientation: string;
flip: boolean;
animationDuration: number = 1000;
animationDuration: number = 900;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;

View File

@@ -58,6 +58,10 @@
<td *ngSwitchCase="'accelerated'"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
</ng-container>
</tr>
<tr *ngIf="!auditEnabled && tx && tx.status === 'accelerated'">
<td class="td-width"></td>
<td><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></td>
</tr>
</tbody>
</table>
</div>

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { of, Subscription, asyncScheduler } from 'rxjs';
import { of, Subscription, asyncScheduler, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
@@ -121,21 +121,37 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.overviewSubscription = block$.pipe(
startWith(null),
pairwise(),
switchMap(([prevBlock, block]) => this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.openGraphService.fail('block-viz-' + this.rawId);
return of([]);
}),
switchMap((transactions) => {
return of({ transactions, direction: 'down' });
})
)
switchMap(([prevBlock, block]) => {
return forkJoin([
this.apiService.getStrippedBlockTransactions$(block.id)
.pipe(
catchError((err) => {
this.overviewError = err;
this.openGraphService.fail('block-viz-' + this.rawId);
return of([]);
}),
switchMap((transactions) => {
return of(transactions);
})
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
}
),
)
.subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => {
.subscribe(([transactions, accelerations]) => {
this.strippedTransactions = transactions;
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.isLoadingOverview = false;
if (this.blockGraph) {
this.blockGraph.destroy();

View File

@@ -42,12 +42,12 @@
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
<td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard [text]="block.id"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
</td>
</tr>
<tr>
@@ -59,7 +59,7 @@
<td [innerHTML]="'&lrm;' + (block.weight | wuBytes: 2)"></td>
</tr>
<tr *ngIf="auditAvailable">
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td><ng-container i18n="latest-blocks.health">Health</ng-container>&nbsp;<a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
<td>
<span
class="health-badge badge"
@@ -233,7 +233,9 @@
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
</ng-template>
</ng-container>
</div>
<div class="col-sm" *ngIf="!isMobile">
@@ -245,7 +247,9 @@
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
</div>
<ng-container *ngIf="network !== 'liquid'">
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
</ng-template>
</ng-container>
</div>
</div>
@@ -452,5 +456,24 @@
</table>
</ng-template>
<ng-template #loadingDetailsSkeletons>
<table class="table table-borderless table-striped audit-details-table">
<tbody>
<tr>
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="block.weight">Weight</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</ng-template>
<br>
<br>

View File

@@ -57,11 +57,6 @@
text-align: left;
}
}
.info-link {
color: rgba(255, 255, 255, 0.4);
margin-left: 5px;
}
.difference {
margin-left: 0.5em;

View File

@@ -328,17 +328,28 @@ export class BlockComponent implements OnInit, OnDestroy {
this.overviewError = err;
return of(null);
})
)
),
this.stateService.env.ACCELERATOR === true && block.height > 819500 ? this.apiService.getAccelerationHistory$({ blockHash: block.id }) : of([])
]);
})
)
.subscribe(([transactions, blockAudit]) => {
.subscribe(([transactions, blockAudit, accelerations]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
this.strippedTransactions = [];
}
const acceleratedInBlock = {};
for (const acc of accelerations) {
acceleratedInBlock[acc.txid] = acc;
}
for (const tx of transactions) {
if (acceleratedInBlock[tx.txid]) {
tx.acc = true;
}
}
this.blockAudit = null;
if (transactions && blockAudit) {
const inTemplate = {};

View File

@@ -46,7 +46,7 @@
</div>
</td>
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
<a

View File

@@ -53,7 +53,7 @@
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="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 [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="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> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem *ngIf="env.BISQ_ENABLED" [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>
@@ -98,4 +98,4 @@
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</ng-container>
</ng-container>

View File

@@ -3,8 +3,8 @@ import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventE
import { StateService } from '../../services/state.service';
import { MempoolBlockDelta, TransactionStripped } from '../../interfaces/websocket.interface';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
import { switchMap, filter } from 'rxjs/operators';
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Router } from '@angular/router';
@@ -33,7 +33,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
poolDirection: string = 'left';
blockSub: Subscription;
deltaSub: Subscription;
rateLimit = 1000;
private lastEventTime = Date.now() - this.rateLimit;
private subId = 0;
firstLoad: boolean = true;
constructor(
public stateService: StateService,
@@ -53,20 +57,81 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngAfterViewInit(): void {
this.blockSub = merge(
of(true),
this.stateService.connectionState$.pipe(filter((state) => state === 2))
)
.pipe(switchMap(() => this.stateService.mempoolBlockTransactions$))
.subscribe((transactionsStripped) => {
this.replaceBlock(transactionsStripped);
});
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
this.updateBlock(delta);
this.stateService.mempoolBlockTransactions$,
this.stateService.mempoolBlockDelta$,
).pipe(
concatMap(update => {
const now = Date.now();
const timeSinceLastEvent = now - this.lastEventTime;
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
const subId = this.subId;
// If time since last event is less than X seconds, delay this event
if (timeSinceLastEvent < this.rateLimit) {
return timer(this.rateLimit - timeSinceLastEvent).pipe(
// Emit the event after the timer
map(() => ({ update, subId }))
);
} else {
// If enough time has passed, emit the event immediately
return of({ update, subId });
}
})
).subscribe(({ update, subId }) => {
// discard stale updates after a block transition
if (subId !== this.subId) {
return;
}
// process update
if (update['added']) {
// delta
this.updateBlock(update as MempoolBlockDelta);
} else {
const transactionsStripped = update as TransactionStripped[];
// new transactions
if (this.firstLoad) {
this.replaceBlock(transactionsStripped);
} else {
const inOldBlock = {};
const inNewBlock = {};
const added: TransactionStripped[] = [];
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
const removed: string[] = [];
for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true;
}
for (const txid of Object.keys(this.blockGraph?.scene?.txs || {})) {
inOldBlock[txid] = true;
if (!inNewBlock[txid]) {
removed.push(txid);
}
}
for (const tx of transactionsStripped) {
if (!inOldBlock[tx.txid]) {
added.push(tx);
} else {
changed.push({
txid: tx.txid,
rate: tx.rate,
acc: tx.acc
});
}
}
this.updateBlock({
removed,
changed,
added
});
}
}
});
}
ngOnChanges(changes): void {
if (changes.index) {
this.subId++;
this.firstLoad = true;
if (this.blockGraph) {
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
}
@@ -77,7 +142,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
ngOnDestroy(): void {
this.blockSub.unsubscribe();
this.deltaSub.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
}

View File

@@ -64,6 +64,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
}),
tap(() => {
this.stateService.markBlock$.next({ mempoolBlockIndex: this.mempoolBlockIndex });
this.websocketService.startTrackMempoolBlock(this.mempoolBlockIndex);
})
);
@@ -74,6 +75,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.websocketService.stopTrackMempoolBlock();
}
getOrdinal(mempoolBlock: MempoolBlock): string {

View File

@@ -224,7 +224,7 @@
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
</td>
<td class="timestamp">
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td class="mined">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>

View File

@@ -115,13 +115,13 @@ export class PoolComponent implements OnInit {
prepareChartOptions(data) {
let title: object;
if (data.length === 0) {
if (data.length <= 1) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
text: $localize`Not enough data yet`,
left: 'center',
top: 'center'
};
@@ -172,14 +172,14 @@ export class PoolComponent implements OnInit {
`;
}.bind(this)
},
xAxis: data.length === 0 ? undefined : {
xAxis: data.length <= 1 ? undefined : {
type: 'time',
splitNumber: (this.isMobile()) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
yAxis: data.length === 0 ? undefined : [
yAxis: data.length <= 1 ? undefined : [
{
min: (value) => {
return value.min * 0.9;
@@ -198,7 +198,7 @@ export class PoolComponent implements OnInit {
}
},
],
series: data.length === 0 ? undefined : [
series: data.length <= 1 ? undefined : [
{
zlevel: 0,
name: 'Hashrate',
@@ -211,7 +211,7 @@ export class PoolComponent implements OnInit {
},
},
],
dataZoom: data.length === 0 ? undefined : [{
dataZoom: data.length <= 1 ? undefined : [{
type: 'inside',
realtime: true,
zoomLock: true,

View File

@@ -1,7 +1,7 @@
<div class="rbf-timeline box" [class.mined]="replacements.mined">
<div class="timeline-wrapper">
<div class="timeline" *ngFor="let timeline of rows">
<div class="intervals">
<div class="timeline" *ngFor="let timeline of rows; let j = index">
<div class="intervals" *ngIf="j < rowLimit || timelineExpanded">
<ng-container *ngFor="let cell of timeline; let i = index;">
<div class="node-spacer"></div>
<ng-container *ngIf="i < timeline.length - 1">
@@ -13,7 +13,7 @@
</ng-container>
</ng-container>
</div>
<div class="nodes">
<div class="nodes" *ngIf="j < rowLimit || timelineExpanded">
<ng-container *ngFor="let cell of timeline; let i = index;">
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
<div class="node"
@@ -37,7 +37,7 @@
</ng-container>
<ng-template #nonNode>
<ng-container [ngSwitch]="cell.connector">
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf" [class.last-pipe]="!timelineExpanded && j === rowLimit - 1"></div></div>
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
<div class="node-spacer" *ngSwitchDefault></div>
</ng-container>
@@ -51,6 +51,16 @@
</div>
</div>
</div>
<div [class.fade-out]="!timelineExpanded && rows.length > rowLimit"></div>
<div class="toggle-wrapper" *ngIf="rows.length > rowLimit && rowLimit !== 0">
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(true);" *ngIf="!timelineExpanded; else collapseBtn">
<span i18n="show-all">Show all</span>
(<ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: rows.length - rowLimit}"></ng-container>)
</button>
<ng-template #collapseBtn>
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTimeline(false);"><span i18n="show-less">Show less</span></button>
</ng-template>
</div>
<ng-template #nodeSpacer>
<div class="node-spacer"></div>
@@ -72,3 +82,5 @@
[isConnector]="hoverConnector"
></app-rbf-timeline-tooltip> -->
</div>
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>

View File

@@ -30,12 +30,32 @@
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.fade-out {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
height: 70px;
top: -70px;
background: linear-gradient(to bottom, rgba(36, 39, 62, 0) 0%, rgba(36, 39, 62, 1) 100%);
z-index: 1;
}
}
.toggle-wrapper {
width: 100%;
text-align: center;
margin: 1.25em 0 0;
}
.intervals, .nodes {
min-width: 100%;
display: flex;
@@ -191,6 +211,10 @@
&.fullrbf {
border-right: solid 10px #1bd8f4;
}
&.last-pipe {
height: 150px;
bottom: -42px;
}
}
.corner {

View File

@@ -25,7 +25,9 @@ function isTimelineCell(val: RbfTree | TimelineCell): boolean {
export class RbfTimelineComponent implements OnInit, OnChanges {
@Input() replacements: RbfTree;
@Input() txid: string;
@Input() rowLimit: number = 5; // If explicitly set to 0, all timelines rows will be displayed by default
rows: TimelineCell[][] = [];
timelineExpanded: boolean = this.rowLimit === 0;
hoverInfo: RbfTree | null = null;
tooltipPosition = null;
@@ -191,6 +193,10 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
return rows;
}
toggleTimeline(expand: boolean): void {
this.timelineExpanded = expand;
}
scrollToSelected() {
const node = document.getElementById('node-' + this.txid);
if (node) {

View File

@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewC
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
import { StateService } from '../../services/state.service';
import { Env, StateService } from '../../services/state.service';
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { ApiService } from '../../services/api.service';
import { SearchResultsComponent } from './search-results/search-results.component';
import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils';
@Component({
selector: 'app-search-form',
@@ -18,7 +19,7 @@ import { SearchResultsComponent } from './search-results/search-results.componen
})
export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
env: Env;
network = '';
assets: object = {};
isSearching = false;
@@ -36,12 +37,13 @@ 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}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/;
regexUnixTimestamp = /^\d{10}$/;
regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
regexBlockhash = getRegex('blockhash', 'mainnet');
regexTransaction = getRegex('transaction');
regexBlockheight = getRegex('blockheight');
regexDate = getRegex('date');
regexUnixTimestamp = getRegex('timestamp');
focus$ = new Subject<string>();
click$ = new Subject<string>();
@@ -66,8 +68,14 @@ export class SearchFormComponent implements OnInit {
}
ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.env = this.stateService.env;
this.stateService.networkChanged$.subscribe((network) => {
this.network = network;
// TODO: Eventually change network type here from string to enum of consts
this.regexAddress = getRegex('address', network as any || 'mainnet');
this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet');
});
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
if (this.searchInput && e.type === EventType.NavigationStart) {
this.searchInput.nativeElement.blur();
@@ -96,9 +104,6 @@ export class SearchFormComponent implements OnInit {
const searchText$ = this.searchForm.get('searchText').valueChanges
.pipe(
map((text) => {
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
return text.substr(1);
}
return text.trim();
}),
tap((text) => {
@@ -132,9 +137,6 @@ export class SearchFormComponent implements OnInit {
);
}),
map((result: any[]) => {
if (this.network === 'bisq') {
result[0] = result[0].map((address: string) => 'B' + address);
}
return result;
}),
tap(() => {
@@ -164,6 +166,7 @@ export class SearchFormComponent implements OnInit {
blockHeight: false,
txId: false,
address: false,
otherNetworks: [],
addresses: [],
nodes: [],
channels: [],
@@ -174,15 +177,21 @@ export class SearchFormComponent implements OnInit {
const addressPrefixSearchResults = result[0];
const lightningResults = result[1];
const matchesBlockHeight = this.regexBlockheight.test(searchText);
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
// Do not show date and timestamp results for liquid and bisq
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet';
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin;
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
const matchesBlockHash = this.regexBlockhash.test(searchText);
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
let matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
if (matchesAddress && this.network === 'bisq') {
searchText = 'B' + searchText;
// Add B prefix to addresses in Bisq network
if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) {
searchText = 'B' + searchText;
matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
}
if (matchesDateTime && searchText.indexOf('/') !== -1) {
@@ -198,7 +207,8 @@ export class SearchFormComponent implements OnInit {
txId: matchesTxId,
blockHash: matchesBlockHash,
address: matchesAddress,
addresses: addressPrefixSearchResults,
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
otherNetworks: otherNetworks,
nodes: lightningResults.nodes,
channels: lightningResults.channels,
};
@@ -217,12 +227,21 @@ export class SearchFormComponent implements OnInit {
selectedResult(result: any): void {
if (typeof result === 'string') {
this.search(result);
} else if (typeof result === 'number') {
} else if (typeof result === 'number' && result <= this.stateService.latestBlockHeight) {
this.navigate('/block/', result.toString());
} else if (result.alias) {
this.navigate('/lightning/node/', result.public_key);
} else if (result.short_id) {
this.navigate('/lightning/channel/', result.id);
} else if (result.network) {
if (result.isNetworkAvailable) {
this.navigate('/address/', result.address, undefined, result.network);
} else {
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}
@@ -230,10 +249,13 @@ export class SearchFormComponent implements OnInit {
const searchText = result || this.searchForm.value.searchText.trim();
if (searchText) {
this.isSearching = true;
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
this.navigate('/address/', searchText);
} else if (this.regexBlockhash.test(searchText) || this.regexBlockheight.test(searchText)) {
} else if (this.regexBlockhash.test(searchText)) {
this.navigate('/block/', searchText);
} else if (this.regexBlockheight.test(searchText)) {
parseInt(searchText) <= this.stateService.latestBlockHeight ? this.navigate('/block/', searchText) : this.isSearching = false;
} else if (this.regexTransaction.test(searchText)) {
const matches = this.regexTransaction.exec(searchText);
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
@@ -256,6 +278,11 @@ export class SearchFormComponent implements OnInit {
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
let timestamp: number;
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
// Check if timestamp is too far in the future or before the genesis block
if (timestamp > Math.floor(Date.now() / 1000)) {
this.isSearching = false;
return;
}
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
(data) => { this.navigate('/block/', data.hash); },
(error) => { console.log(error); this.isSearching = false; }
@@ -267,12 +294,17 @@ export class SearchFormComponent implements OnInit {
}
}
navigate(url: string, searchText: string, extras?: any): void {
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'bisq' | 'mempool', swapNetwork as Network)) {
window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env);
} else {
this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
this.searchTriggered.emit();
this.searchForm.setValue({
searchText: '',
});
this.isSearching = false;
}
}
}

View File

@@ -1,4 +1,4 @@
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
<ng-template [ngIf]="results.blockHeight">
<div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div>
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
@@ -35,10 +35,18 @@
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
</button>
</ng-template>
<ng-template [ngIf]="results.otherNetworks.length">
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 20 : 25 }"></ng-container>&nbsp;<b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
</button>
</ng-template>
</ng-template>
<ng-template [ngIf]="results.addresses.length">
<div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div>
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
</button>
</ng-template>
@@ -46,7 +54,7 @@
<ng-template [ngIf]="results.nodes.length">
<div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div>
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
</button>
</ng-template>
@@ -54,7 +62,7 @@
<ng-template [ngIf]="results.channels.length">
<div class="card-title" i18n="search.lightning-channels">Lightning Channels</div>
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> &nbsp;<span class="symbol">{{ channel.id }}</span>
</button>
</ng-template>

View File

@@ -7,6 +7,10 @@
margin-left: 10px;
}
.danger {
color: #dc3545;
}
.dropdown-menu {
position: absolute;
top: 42px;

View File

@@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
ngOnChanges() {
this.activeIdx = 0;
if (this.results) {
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
}
}
@@ -45,6 +45,9 @@ export class SearchResultsComponent implements OnChanges {
break;
case 'Enter':
event.preventDefault();
if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) {
return;
}
if (this.resultsFlattened[this.activeIdx]) {
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
} else {

View File

@@ -84,6 +84,14 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.5v-2a3 3 0 116 0v2c0 1.11-.603 2.08-1.5 2.599v1.224a1 1 0 00.629.928l2.05.82A3.693 3.693 0 0118.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 00.629-.929v-1.224A2.999 2.999 0 019 9.5z"></path>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'goggles'">
<svg viewBox="0 0 558.56415 255.62396" [attr.width]="width" [attr.height]="height" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="m 466.62029,0.15290693 c 2.84,0 5.90333,0.296667 9.19,0.88999997 17.05333,3.066667 31.92333,10.4666671 44.61,22.2000001 15.02,13.88 24.94,35.04 25.46,54.77 0.0133,0.54 0.26,0.93 0.74,1.17 5.05,2.52 9.14,6.28 10.82,11.39 0.79333,2.38667 1.16667,6.12 1.12,11.200003 -0.14,15.94667 -0.19,30.22667 -0.15,42.84 0.03,8.92 -3.88,14.6 -11.36,19.02 -0.71869,0.42495 -1.17676,1.1834 -1.22,2.02 -0.33,6.26 -0.72,13.09 -2.3,19.16 -2.2,8.49 -5.1,16.06 -9.74,23.78 -1.71333,2.85333 -3.41333,5.77333 -5.1,8.76 -2.64,4.68 -4.99,8.03667 -7.05,10.07 -2.45,2.43 -4.45,5.13 -7.02,7.38 -11.13,9.75 -26.36,16.58 -40.92,19.39 -5.05333,0.98 -11.52667,1.45667 -19.42,1.43 -25.96,-0.0667 -51.90333,-0.12 -77.83,-0.16 -10.08,-0.01 -20.45,-1.66 -29.9,-6 -15.07333,-6.92 -26.44667,-17.19667 -34.12,-30.83 -3.17,-5.64 -5.15,-11.78 -8.42,-17.3 -4.07333,-6.87333 -10.09,-11.46 -18.05,-13.76 -12.38,-3.57 -25.31,2.57 -32.13,13.15 -2.71,4.19 -4.3,9.04 -6.77,13.41 -1.99,3.52 -3.53,7.35 -5.7,10.78 -3.36667,5.34667 -5.54,8.46667 -6.52,9.36 -1.70667,1.56667 -3.5,3.16333 -5.38,4.79 -7.94,6.88 -18.64,11.02 -30.11,14.12 -5.35333,1.44667 -10.01667,2.18333 -13.99,2.21 -17.9,0.13333 -42.16667,0.17667 -72.8,0.13 -6.71333,-0.007 -13.376669,-0.14667 -19.990002,-0.42 -9.086667,-0.36667 -18.55,-2.88667 -28.39,-7.56 -18.233333,-8.65333 -32.26,-22.45333 -42.08,-41.4 -6.913333,-13.34667 -9.993333,-26.95 -9.24,-40.81 0.03144,-0.59686 -0.306835,-1.15004 -0.85,-1.39 -8.8400001,-3.98 -12.00000011,-10.78 -11.96000011,-20.36 0.05333333,-16.54667 0.03666667,-32.38 -0.0500000043,-47.500003 -0.0399999957,-7.54 4.09000001427,-13.94 11.34000011427,-16.48 0.543871,-0.19451 0.915559,-0.69668 0.94,-1.27 1.78,-39.54 32.94,-72.5000001 71.86,-77.57000007 4.18,-0.54 10.406667,-0.793333 18.680002,-0.76 363.8,0.15 0,0 363.8,0.15 z m 1.98,216.71000307 c 11.01,-2.05 20.88,-8.4 27.78,-17.23 7.51,-9.63 13.42,-21.1 13.47,-33.01 0.12,-25.71333 0.21667,-52.20667 0.29,-79.480003 0.0133,-6.34 -0.47333,-11.32 -1.46,-14.94 -3,-11 -9.03667,-19.66333 -18.11,-25.99 -7.32,-5.113333 -14.88667,-7.693333 -22.7,-7.74 -41.5,-0.233333 -85.66667,-0.276667 -132.5,-0.13 -62.12,0.206667 -142.78667,0.23 -242.000002,0.07 -12.52,-0.02 -23.406667,4.326667 -32.66,13.04 -7.79,7.34 -12.17,18.06 -12.13,28.98 0.09333,27.086673 0.06,54.470003 -0.1,82.150003 -0.03333,5.97333 0.393333,10.85333 1.28,14.64 4.28,18.33 19.71,34.9 37.96,39.18 3.7,0.86667 8.936667,1.27333 15.710002,1.22 24.08667,-0.18667 50.07,-0.19 77.95,-0.01 2.99333,0.02 5.98667,-0.41 8.98,-1.29 16.42,-4.84 24.52,-15.8 31.08,-30.65 1.76667,-3.99333 4.16333,-8.27 7.19,-12.83 7.95,-11.98 19.14,-18.59 32.66,-22.45 6.32667,-1.80667 11.84667,-2.70333 16.56,-2.69 25.87,0.07 47.99,12.39 58.26,35.87 1.54,3.52667 3.25333,7.07333 5.14,10.64 6.20667,11.73333 15.25333,19.07333 27.14,22.02 3.79333,0.94667 10.03667,1.39333 18.73,1.34 23.82667,-0.13333 47.73,-0.13667 71.71,-0.01 3.92,0.02 7.17667,-0.21333 9.77,-0.7 z" id="outline" />
<path fill="currentColor" opacity="0.3" d="m 496.97029,199.03291 c -6.9,8.83 -16.77,15.18 -27.78,17.23 q -3.89,0.73 -9.77,0.7 -35.97,-0.19 -71.71,0.01 -13.04,0.08 -18.73,-1.34 -17.83,-4.42 -27.14,-22.02 -2.83,-5.35 -5.14,-10.64 c -10.27,-23.48 -32.39,-35.8 -58.26,-35.87 q -7.07,-0.02 -16.56,2.69 c -13.52,3.86 -24.71,10.47 -32.66,22.45 q -4.54,6.84 -7.19,12.83 c -6.56,14.85 -14.66,25.81 -31.08,30.65 q -4.49,1.32 -8.98,1.29 -41.82,-0.27 -77.95,0.01 -10.160002,0.08 -15.710002,-1.22 c -18.25,-4.28 -33.68,-20.85 -37.96,-39.18 q -1.33,-5.68 -1.28,-14.64 0.24,-41.52 0.1,-82.150003 c -0.04,-10.92 4.34,-21.64 12.13,-28.98 q 13.88,-13.07 32.66,-13.04 148.820002,0.24 242.000002,-0.07 70.25,-0.22 132.5,0.13 11.72,0.07 22.7,7.74 13.61,9.49 18.11,25.99 1.48,5.43 1.46,14.94 -0.11,40.910003 -0.29,79.480003 c -0.05,11.91 -5.96,23.38 -13.47,33.01 z m -8.14,-101.340003 c 5.11,-2.24 9.54,-9.21 6.39,-14.8 q -1.59,-2.82 -4.29,-5.41 -8.04,-7.73 -15.91,-15.96 -2.88,-3.02 -5.51,-4.19 c -6.41,-2.84 -13.19,1.02 -15.6,7.25 -1.35,3.51 0.64,7.36 3.07,9.77 q 9.48,9.38 20.18,20.59 5.3,5.550003 11.67,2.75 z m -404.320002,-6.02 c 4.77,-6.16 10.61,-11.82 16.350002,-17.36 q 4.6,-4.45 3.41,-9.38 c -1.57,-6.47 -9.240002,-9.94 -15.220002,-7.57 q -2.72,1.07 -7.3,5.87 -7.54,7.9 -15.2,15.77 -2.83,2.9 -3.54,6.23 c -1.67,7.85 5.38,14.06 12.94,13.26 3.57,-0.39 6.5,-4.17 8.56,-6.82 z" id="lens" />
<path fill="currentColor" d="m 488.83029,97.692907 q -6.37,2.800003 -11.67,-2.75 -10.7,-11.21 -20.18,-20.59 c -2.43,-2.41 -4.42,-6.26 -3.07,-9.77 2.41,-6.23 9.19,-10.09 15.6,-7.25 q 2.63,1.17 5.51,4.19 7.87,8.23 15.91,15.96 2.7,2.59 4.29,5.41 c 3.15,5.59 -1.28,12.56 -6.39,14.8 z" id="glint-a" />
<path fill="currentColor" d="m 84.510288,91.672907 c -2.06,2.65 -4.99,6.43 -8.56,6.82 -7.56,0.8 -14.61,-5.41 -12.94,-13.26 q 0.71,-3.33 3.54,-6.23 7.66,-7.87 15.2,-15.77 4.58,-4.8 7.3,-5.87 c 5.98,-2.37 13.650002,1.1 15.220002,7.57 q 1.19,4.93 -3.41,9.38 c -5.740002,5.54 -11.580002,11.2 -16.350002,17.36 z" id="glint-b" />
</svg>
</ng-container>
</ng-container>
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">

View File

@@ -62,6 +62,7 @@
<tr><td>mempool.space</td></tr>
<tr><td>Be your own explorer</td></tr>
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
<tr><td>Mempool Goggles</td></tr>
</tbody>
</table>
</div>
@@ -314,7 +315,7 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&trade;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<li>What to Do When You See Abuse</li>
<br>

View File

@@ -299,7 +299,11 @@
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr *ngIf="adjustedVsize != null">
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize</td>
<td i18n="transaction.adjusted-vsize|Transaction Adjusted VSize">Adjusted vsize
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-is-adjusted-vsize">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (adjustedVsize | vbytes: 2)"></td>
</tr>
<tr>
@@ -321,7 +325,11 @@
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td>
</tr>
<tr *ngIf="sigops != null">
<td i18n="transaction.sigops|Transaction Sigops">Sigops</td>
<td i18n="transaction.sigops|Transaction Sigops">Sigops
<a class="info-link" [routerLink]="['/docs/faq/' | relativeUrl]" fragment="what-are-sigops">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</td>
<td [innerHTML]="'&lrm;' + (sigops | number)"></td>
</tr>
<tr>

View File

@@ -243,6 +243,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.fetchAccelerationSubscription = this.fetchAcceleration$.pipe(
filter(() => this.stateService.env.ACCELERATOR === true),
tap(() => {
this.accelerationInfo = null;
}),
@@ -440,7 +441,11 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
block_time: block.timestamp,
};
this.stateService.markBlock$.next({ blockHeight: block.height });
this.audioService.playSound('magic');
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'mined', 'completed'].includes(this.accelerationInfo.status))) {
this.audioService.playSound('wind-chimes-harp-ascend');
} else {
this.audioService.playSound('magic');
}
this.fetchAcceleration$.next(block.id);
}
});