Merge branch 'master' into nymkappa/hashrate-indexing-message
This commit is contained in:
@@ -406,27 +406,23 @@
|
||||
<div class="copyright">
|
||||
<div class="title">
|
||||
Copyright © 2019-2023<br>
|
||||
The Mempool Open Source Project
|
||||
Mempool Space K.K.<br>
|
||||
and other shadowy super-coders
|
||||
</div>
|
||||
<p>
|
||||
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of (at your option) either:<br>
|
||||
<a href="https://github.com/mempool/mempool">The Mempool Open Source Project</a> is free software; you can redistribute it and/or modify it under the terms of the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
1) the <a href="https://www.gnu.org/licenses/agpl-3.0-standalone.html">GNU Affero General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>; or<br>
|
||||
</li>
|
||||
<li>
|
||||
2) the <a href="https://www.gnu.org/licenses/gpl-3.0-standalone.html">GNU General Public License</a> as published by the Free Software Foundation, either version 3 of the License or any later version approved by a proxy statement published on <https://mempool.space/about>.<br>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the full license terms for more details.<br>
|
||||
</p>
|
||||
<p>
|
||||
This program incorporates software and other components licensed from third parties. See the full list of <a href="https://mempool.space/3rdpartylicenses.txt">Third-Party Licenses</a> for legal notices from those projects.
|
||||
</p>
|
||||
<div class="title">
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, 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 <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
></app-accelerate-fee-graph>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<ng-container *ngIf="estimate else loadingEstimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
|
||||
<div *ngIf="user && !estimate.hasAccess">
|
||||
@@ -243,4 +243,9 @@
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingEstimate>
|
||||
<div class="skeleton-loader"></div>
|
||||
<br>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<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" 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;">
|
||||
<button class="btn filter-tag selected" (click)="toggleFilter(filter)">{{ filters[filter].label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
|
||||
<ng-container *ngFor="let group of filterGroups;">
|
||||
<h5>{{ group.label }}</h5>
|
||||
<div class="filter-group">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="filter-menu" *ngIf="menuOpen && cssWidth <= 280">
|
||||
<ng-container *ngFor="let group of filterGroups;">
|
||||
<ng-container *ngFor="let filter of group.filters;">
|
||||
<button *ngIf="filter.important" class="btn filter-tag" [class.selected]="filterFlags[filter.key]" (click)="toggleFilter(filter.key)">{{ filter.label }}</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,142 @@
|
||||
.block-filters {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1em;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
|
||||
.filter-bar, .active-tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.active-tags {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.25em;
|
||||
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;
|
||||
background: none;
|
||||
border: solid 2px white;
|
||||
border-radius: 0.35em;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.filter-menu {
|
||||
h5 {
|
||||
font-size: 0.8rem;
|
||||
color: white;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.25em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
font-size: 0.9em;
|
||||
background: #181b2daf;
|
||||
border: solid 1px #105fb0;
|
||||
color: white;
|
||||
border-radius: 0.2rem;
|
||||
padding: 0.2em 0.5em;
|
||||
transition: background-color 300ms;
|
||||
margin-right: 0.25em;
|
||||
pointer-events: all;
|
||||
|
||||
&.selected {
|
||||
background-color: #105fb0;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.block-overview-graph:hover) &, &:hover, &:active {
|
||||
.menu-toggle {
|
||||
opacity: 0.5;
|
||||
background: #181b2d;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open, &.filters-active {
|
||||
.menu-toggle {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open, &.filters-active {
|
||||
.menu-toggle {
|
||||
opacity: 1;
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
background: #181b2d7f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
pointer-events: all;
|
||||
background: #181b2d7f;
|
||||
}
|
||||
|
||||
&.small {
|
||||
.filter-tag {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
&.vsmall {
|
||||
.filter-menu {
|
||||
margin-top: 0.25em;
|
||||
h5 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.filter-tag {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
&.tiny {
|
||||
.filter-tag {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FilterGroups, TransactionFilters } from '../../shared/filters.utils';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-filters',
|
||||
templateUrl: './block-filters.component.html',
|
||||
styleUrls: ['./block-filters.component.scss'],
|
||||
})
|
||||
export class BlockFiltersComponent implements OnChanges {
|
||||
@Input() cssWidth: number = 800;
|
||||
@Output() onFilterChanged: EventEmitter<bigint | null> = new EventEmitter();
|
||||
|
||||
filters = TransactionFilters;
|
||||
filterGroups = FilterGroups;
|
||||
activeFilters: string[] = [];
|
||||
filterFlags: { [key: string]: boolean } = {};
|
||||
menuOpen: boolean = false;
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.cssWidth) {
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
toggleFilter(key): void {
|
||||
const filter = this.filters[key];
|
||||
this.filterFlags[key] = !this.filterFlags[key];
|
||||
if (this.filterFlags[key]) {
|
||||
// remove any other flags in the same toggle group
|
||||
if (filter.toggle) {
|
||||
this.activeFilters.forEach(f => {
|
||||
if (this.filters[f].toggle === filter.toggle) {
|
||||
this.filterFlags[f] = false;
|
||||
}
|
||||
});
|
||||
this.activeFilters = this.activeFilters.filter(f => this.filters[f].toggle !== filter.toggle);
|
||||
}
|
||||
// add new active filter
|
||||
this.activeFilters.push(key);
|
||||
} else {
|
||||
// remove active filter
|
||||
this.activeFilters = this.activeFilters.filter(f => f != key);
|
||||
}
|
||||
this.onFilterChanged.emit(this.getBooleanFlags());
|
||||
}
|
||||
|
||||
getBooleanFlags(): bigint | null {
|
||||
let flags = 0n;
|
||||
for (const key of Object.keys(this.filterFlags)) {
|
||||
if (this.filterFlags[key]) {
|
||||
flags |= this.filters[key].flag;
|
||||
}
|
||||
}
|
||||
return flags || null;
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
onClick(event): boolean {
|
||||
// click away from menu
|
||||
if (!event.target.closest('button')) {
|
||||
this.menuOpen = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,6 @@
|
||||
[auditEnabled]="auditHighlighting"
|
||||
[blockConversion]="blockConversion"
|
||||
></app-block-overview-tooltip>
|
||||
<app-block-filters *ngIf="showFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,19 @@ import { Color, Position } from './sprite-types';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils';
|
||||
|
||||
const unmatchedOpacity = 0.2;
|
||||
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity));
|
||||
const unmatchedAuditColors = {
|
||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
||||
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
||||
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity),
|
||||
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-graph',
|
||||
@@ -26,6 +39,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Input() unavailable: boolean = false;
|
||||
@Input() auditHighlighting: boolean = false;
|
||||
@Input() showFilters: boolean = false;
|
||||
@Input() filterFlags: bigint | null = null;
|
||||
@Input() blockConversion: Price;
|
||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
||||
@@ -92,9 +107,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (changes.auditHighlighting) {
|
||||
this.setHighlightingEnabled(this.auditHighlighting);
|
||||
}
|
||||
if (changes.overrideColor) {
|
||||
if (changes.overrideColor && this.scene) {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
if ((changes.filterFlags || changes.showFilters) && this.scene) {
|
||||
this.setFilterFlags(this.filterFlags);
|
||||
}
|
||||
}
|
||||
|
||||
setFilterFlags(flags: bigint | null): void {
|
||||
if (flags != null) {
|
||||
this.scene.setColorFunction(this.getFilterColorFunction(flags));
|
||||
} else {
|
||||
this.scene.setColorFunction(this.overrideColors);
|
||||
}
|
||||
this.start();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -374,6 +401,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
onPointerMove(event) {
|
||||
if (event.target === this.canvas.nativeElement) {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, false);
|
||||
} else {
|
||||
this.onPointerLeave(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +503,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
onTxHover(hoverId: string) {
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
}
|
||||
|
||||
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) {
|
||||
return (tx: TxView) => {
|
||||
if ((tx.bigintFlags & flags) === flags) {
|
||||
return defaultColorFunction(tx);
|
||||
} else {
|
||||
return defaultColorFunction(
|
||||
tx,
|
||||
unmatchedFeeColors,
|
||||
unmatchedAuditFeeColors,
|
||||
unmatchedMarginalFeeColors,
|
||||
unmatchedAuditColors
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL shader attributes
|
||||
|
||||
@@ -2,19 +2,7 @@ import { FastVertexArray } from './fast-vertex-array';
|
||||
import TxView from './tx-view';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { darken, desaturate, hexToColor } from './utils';
|
||||
|
||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||
const auditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
import { defaultColorFunction } from './utils';
|
||||
|
||||
export default class BlockScene {
|
||||
scene: { count: number, offset: { x: number, y: number}};
|
||||
@@ -27,6 +15,7 @@ export default class BlockScene {
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
width: number;
|
||||
height: number;
|
||||
gridWidth: number;
|
||||
@@ -78,7 +67,7 @@ export default class BlockScene {
|
||||
}
|
||||
|
||||
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
|
||||
this.getColor = colorFunction;
|
||||
this.getColor = colorFunction || defaultColorFunction;
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
this.updateColors(performance.now(), 50);
|
||||
@@ -277,6 +266,20 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
||||
}
|
||||
|
||||
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
const txColor = this.getColor(tx);
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
color: txColor
|
||||
},
|
||||
duration: animate ? (duration || this.animationDuration) : 1,
|
||||
start: startTime,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
this.saveGridToScreenPosition(tx);
|
||||
@@ -325,7 +328,7 @@ export default class BlockScene {
|
||||
} else {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
position: tx.screenPosition,
|
||||
},
|
||||
duration: animate ? this.animationDuration : 0,
|
||||
minDuration: animate ? (this.animationDuration / 2) : 0,
|
||||
@@ -903,49 +906,4 @@ class BlockLayout {
|
||||
|
||||
function feeRateDescending(a: TxView, b: TxView) {
|
||||
return b.feerate - a.feerate;
|
||||
}
|
||||
|
||||
function defaultColorFunction(tx: TxView): Color {
|
||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!tx.scene?.highlightingEnabled) {
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
switch(tx.status) {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (tx.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import TxSprite from './tx-sprite';
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
|
||||
import { hexToColor } from './utils';
|
||||
import BlockScene from './block-scene';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
@@ -29,6 +29,7 @@ export default class TxView implements TransactionStripped {
|
||||
feerate: number;
|
||||
acc?: boolean;
|
||||
rate?: number;
|
||||
bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
@@ -57,6 +58,7 @@ export default class TxView implements TransactionStripped {
|
||||
this.acc = tx.acc;
|
||||
this.rate = tx.rate;
|
||||
this.status = tx.status;
|
||||
this.bigintFlags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
this.initialised = false;
|
||||
this.vertexArray = scene.vertexArray;
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
import { Color } from './sprite-types';
|
||||
import TxView from './tx-view';
|
||||
|
||||
export function hexToColor(hex: string): Color {
|
||||
return {
|
||||
@@ -25,5 +27,75 @@ export function darken(color: Color, amount: number): Color {
|
||||
g: color.g * amount,
|
||||
b: color.b * amount,
|
||||
a: color.a,
|
||||
};
|
||||
}
|
||||
|
||||
export function setOpacity(color: Color, opacity: number): Color {
|
||||
return {
|
||||
...color,
|
||||
a: opacity
|
||||
};
|
||||
}
|
||||
|
||||
// precomputed colors
|
||||
export const defaultFeeColors = mempoolFeeColors.map(hexToColor);
|
||||
export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||
export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||
export const defaultAuditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
accelerated: hexToColor('8F5FF6'),
|
||||
};
|
||||
|
||||
export function defaultColorFunction(
|
||||
tx: TxView,
|
||||
feeColors: Color[] = defaultFeeColors,
|
||||
auditFeeColors: Color[] = defaultAuditFeeColors,
|
||||
marginalFeeColors: Color[] = defaultMarginalFeeColors,
|
||||
auditColors: { [status: string]: Color } = defaultAuditColors
|
||||
): Color {
|
||||
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1;
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Normal mode
|
||||
if (!tx.scene?.highlightingEnabled) {
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
return feeLevelColor;
|
||||
}
|
||||
// Block audit
|
||||
switch(tx.status) {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'accelerated':
|
||||
return auditColors.accelerated;
|
||||
case 'found':
|
||||
if (tx.context === 'projected') {
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
default:
|
||||
if (tx.acc) {
|
||||
return auditColors.accelerated;
|
||||
} else {
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { Position } from '../../components/block-overview-graph/sprite-types.js';
|
||||
import { Price } from '../../services/price.service';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-overview-tooltip',
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td>‎<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>‎<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>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="timeLtr ? 'right' : 'left'"
|
||||
[flip]="true"
|
||||
[showFilters]="showFilters"
|
||||
[overrideColors]="overrideColors"
|
||||
(txClickEvent)="onTxClick($event)"
|
||||
></app-block-overview-graph>
|
||||
|
||||
@@ -18,6 +18,7 @@ import TxView from '../block-overview-graph/tx-view';
|
||||
})
|
||||
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
|
||||
@Input() index: number;
|
||||
@Input() showFilters: boolean = false;
|
||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
||||
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="block-wrapper">
|
||||
<div class="block-container">
|
||||
<app-mempool-block-overview [index]="index"></app-mempool-block-overview>
|
||||
<app-mempool-block-overview [index]="index" [showFilters]="true"></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,6 +27,7 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy {
|
||||
autofit: boolean = false;
|
||||
resolution: number = 80;
|
||||
index: number = 0;
|
||||
filterFlags: bigint | null = 0n;
|
||||
|
||||
routeParamsSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
@@ -38,6 +39,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
window['setFlags'] = this.setFilterFlags.bind(this);
|
||||
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
|
||||
this.routeParamsSubscription = this.route.paramMap
|
||||
@@ -82,4 +85,8 @@ export class MempoolBlockViewComponent implements OnInit, OnDestroy {
|
||||
this.routeParamsSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
setFilterFlags(flags: bigint | null) {
|
||||
this.filterFlags = flags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||
</div>
|
||||
<div class="col-md chart-container">
|
||||
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
|
||||
<div class="block-with-filters" *ngIf="webGlEnabled">
|
||||
<app-mempool-block-overview [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)" [showFilters]="true"></app-mempool-block-overview>
|
||||
</div>
|
||||
<app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" [feeRange]="mempoolBlock.isStack ? mempoolBlock.feeRange : []" [vsize]="mempoolBlock.blockVSize" ></app-fee-distribution-graph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
||||
@@ -28,6 +28,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const addressPrefixSearchResults = result[0];
|
||||
const lightningResults = result[1];
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
@@ -217,7 +217,7 @@ 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);
|
||||
@@ -232,8 +232,10 @@ export class SearchFormComponent implements OnInit {
|
||||
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') {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 19, 2021</h6>
|
||||
<h6>Updated: December 7, 2023</h6>
|
||||
<br>
|
||||
|
||||
<div class="text-left">
|
||||
@@ -56,7 +56,12 @@
|
||||
<tbody>
|
||||
<tr><td>Mempool Space K.K.</td></tr>
|
||||
<tr><td>The Mempool Open Source Project</td></tr>
|
||||
<tr><td>Mempool Accelerator</td></tr>
|
||||
<tr><td>Mempool Enterprise</td></tr>
|
||||
<tr><td>Mempool Liquidity</td></tr>
|
||||
<tr><td>mempool.space</td></tr>
|
||||
<tr><td>Be your own explorer</td></tr>
|
||||
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -89,11 +94,16 @@
|
||||
<p>The mempool Square Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-blocks.png" style="width: 500px; max-width: 80%">
|
||||
<img src="/resources/mempool-blocks-2-3-logo.jpeg" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool Blocks Logo</p>
|
||||
<br><br>
|
||||
|
||||
<img src="/resources/mempool-blocks-3-2-logo.jpeg" style="width: 500px; max-width: 80%">
|
||||
<br><br>
|
||||
<p>The mempool Blocks 3 | 2 Logo</p>
|
||||
<br><br>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -304,8 +314,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 Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks 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®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem™, 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>
|
||||
|
||||
@@ -8911,6 +8911,13 @@ export const faqData = [
|
||||
fragment: "what-is-block-health",
|
||||
title: "What is block health?",
|
||||
},
|
||||
{
|
||||
type: "endpoint",
|
||||
category: "advanced",
|
||||
showConditions: bitcoinNetworks,
|
||||
fragment: "how-do-mempool-goggles-work",
|
||||
title: "How do Mempool Goggles work?",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
category: "self-hosting",
|
||||
|
||||
@@ -279,6 +279,95 @@
|
||||
<p class='note'>Because of this feature's resource usage and availability requirements, it is only supported on official mempool.space instances.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-do-mempool-goggles-work">
|
||||
<p>Mempool Goggles are a set of filters that can be applied to the <a [routerLink]="['/mempool-block/0' | relativeUrl]">mempool block visualizations</a> to highlight different types of transactions.</p>
|
||||
<p>There are currently 25 different Mempool Goggles filters, grouped into six categories:</p>
|
||||
<dl>
|
||||
<dt>Features</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
<dt>RBF enabled</dt>
|
||||
<dd>The transaction opts-in to BIP-125 replaceability.</dd>
|
||||
<dt>RBF disabled</dt>
|
||||
<dd>The transaction does not opt-in to BIP-125 replaceability.</dd>
|
||||
<dt>Version 1</dt>
|
||||
<dd>The default version for most transactions.</dd>
|
||||
<dt>Version 2</dt>
|
||||
<dd>Required for transactions which use OP_CHECKSEQUENCEVERIFY relative timelocks.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
|
||||
<dt>Address Types</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
<dt>P2PK</dt>
|
||||
<dd>Pay-to-public-key. A legacy output format most commonly found in old coinbase transactions.</dd>
|
||||
<dt>Bare multisig</dt>
|
||||
<dd>A legacy form of multisig, most commonly used for data embedding schemes (see also "Fake pubkey").</dd>
|
||||
<dt>P2PKH</dt>
|
||||
<dd>Pay-to-public-key-hash. A legacy address type that locks outputs to a public key.</dd>
|
||||
<dt>P2SH</dt>
|
||||
<dd>Pay-to-script-hash. A legacy address type that locks outputs to a <em>redeem script</em>.</dd>
|
||||
<dt>P2WPKH</dt>
|
||||
<dd>Pay-to-witness-public-key-hash. The SegWit version of P2PKH.</dd>
|
||||
<dt>P2WSH</dt>
|
||||
<dd>Pay-to-witness-script-hash. The SegWit version of P2SH.</dd>
|
||||
<dt>Taproot</dt>
|
||||
<dd>Addresses using the SegWit V1 format added in the Taproot upgrade.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
|
||||
<dt>Behavior</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
<dt>Paid for by child</dt>
|
||||
<dd>The transaction's effective fee rate has been increased by a higher rate CPFP child.</dd>
|
||||
<dt>Pays for parent</dt>
|
||||
<dd>The transaction bumps the effective fee rate of a lower rate CPFP ancestor.</dd>
|
||||
<dt>Replacement</dt>
|
||||
<dd>The transaction replaced a prior version via RBF.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
|
||||
<dt>Data</dt>
|
||||
<dd>
|
||||
Different methods of embedding arbitrary data in a Bitcoin transaction.
|
||||
<dl>
|
||||
<dt>OP_RETURN</dt>
|
||||
<dt>Fake pubkey</dt>
|
||||
<dd>Data may be embedded in an invalid public key in a P2PK or Bare multisig output. This is a heuristic filter and can be prone to false positives and false negatives.</dd>
|
||||
<dt>Inscription</dt>
|
||||
<dd>Data is embedded in the witness script of a taproot input.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
|
||||
<dt>Heuristics</dt>
|
||||
<dd>
|
||||
These filters match common types of transactions according to subjective criteria.
|
||||
<dl>
|
||||
<dt>Coinjoin</dt>
|
||||
<dd>A type of collaborative privacy-improving transaction.</dd>
|
||||
<dt>Consolidation</dt>
|
||||
<dd>The transaction condenses many inputs into a few outputs.</dd>
|
||||
<dt>Batch payment</dt>
|
||||
<dd>The transaction sends coins from a few inputs to many outputs.</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
|
||||
<dt>Sighash Flags</dt>
|
||||
<dd>
|
||||
Different ways of signing inputs to Bitcoin transactions. Note that selecting multiple sighash filters will highlight transactions in which each sighash flag is used, but not necessarily in the same input.
|
||||
<dl>
|
||||
<dt>sighash_all</dt>
|
||||
<dt>sighash_none</dt>
|
||||
<dt>sighash_single</dt>
|
||||
<dt>sighash_default</dt>
|
||||
<dt>sighash_anyonecanpay</dt>
|
||||
</dl>
|
||||
</dd>
|
||||
</dl>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="who-runs-this-website">
|
||||
The official mempool.space website is operated by The Mempool Open Source Project. See more information on our <a [routerLink]="['/about']">About page</a>. There are also many unofficial instances of this website operated by individual members of the Bitcoin community.
|
||||
</ng-template>
|
||||
|
||||
@@ -389,3 +389,44 @@ h3 {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* styles for nested definition lists */
|
||||
dl {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
color: #4a68b9;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
padding: 2px 0;
|
||||
|
||||
& > dl {
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #4a68b9;
|
||||
margin-left: 1em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
& > dl > dt {
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
color: #e83e8c;
|
||||
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& > dl > dd {
|
||||
display: inline;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +180,7 @@ export interface TransactionStripped {
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
acc?: boolean;
|
||||
flags?: number | null;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface TransactionStripped {
|
||||
value: number;
|
||||
acc?: boolean; // is accelerated?
|
||||
rate?: number; // effective fee rate
|
||||
flags?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||
|
||||
@@ -9,6 +9,8 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
|
||||
})
|
||||
export class LightningApiService {
|
||||
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
@@ -23,6 +25,46 @@ export class LightningApiService {
|
||||
});
|
||||
}
|
||||
|
||||
private generateCacheKey(functionName: string, params: any[]): string {
|
||||
return functionName + JSON.stringify(params);
|
||||
}
|
||||
|
||||
// delete expired cache entries
|
||||
private cleanExpiredCache(): void {
|
||||
this.requestCache.forEach((value, key) => {
|
||||
if (value.expiry < Date.now()) {
|
||||
this.requestCache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cachedRequest<T, F extends (...args: any[]) => Observable<T>>(
|
||||
apiFunction: F,
|
||||
expireAfter: number, // in ms
|
||||
...params: Parameters<F>
|
||||
): Observable<T> {
|
||||
this.cleanExpiredCache();
|
||||
|
||||
const cacheKey = this.generateCacheKey(apiFunction.name, params);
|
||||
if (!this.requestCache.has(cacheKey)) {
|
||||
const subject = new BehaviorSubject<T | null>(null);
|
||||
this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter });
|
||||
|
||||
apiFunction.bind(this)(...params).pipe(
|
||||
tap(data => {
|
||||
subject.next(data as T);
|
||||
}),
|
||||
catchError((error) => {
|
||||
subject.error(error);
|
||||
return of(null);
|
||||
}),
|
||||
shareReplay(1),
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1));
|
||||
}
|
||||
|
||||
getNode$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.listStatistics$(timespan)
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
const chartData = {
|
||||
tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]),
|
||||
|
||||
@@ -81,9 +81,9 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.listStatistics$(timespan)
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
this.prepareChartOptions({
|
||||
channel_count: data.map(val => [val.added * 1000, val.channel_count]),
|
||||
|
||||
88
frontend/src/app/shared/filters.utils.ts
Normal file
88
frontend/src/app/shared/filters.utils.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
export interface Filter {
|
||||
key: string,
|
||||
label: string,
|
||||
flag: bigint,
|
||||
toggle?: string,
|
||||
group?: string,
|
||||
important?: boolean,
|
||||
}
|
||||
|
||||
// binary flags for transaction classification
|
||||
export const TransactionFlags = {
|
||||
// features
|
||||
rbf: 0b00000001n,
|
||||
no_rbf: 0b00000010n,
|
||||
v1: 0b00000100n,
|
||||
v2: 0b00001000n,
|
||||
multisig: 0b00010000n,
|
||||
// address types
|
||||
p2pk: 0b00000001_00000000n,
|
||||
p2ms: 0b00000010_00000000n,
|
||||
p2pkh: 0b00000100_00000000n,
|
||||
p2sh: 0b00001000_00000000n,
|
||||
p2wpkh: 0b00010000_00000000n,
|
||||
p2wsh: 0b00100000_00000000n,
|
||||
p2tr: 0b01000000_00000000n,
|
||||
// behavior
|
||||
cpfp_parent: 0b00000001_00000000_00000000n,
|
||||
cpfp_child: 0b00000010_00000000_00000000n,
|
||||
replacement: 0b00000100_00000000_00000000n,
|
||||
// data
|
||||
op_return: 0b00000001_00000000_00000000_00000000n,
|
||||
fake_pubkey: 0b00000010_00000000_00000000_00000000n,
|
||||
inscription: 0b00000100_00000000_00000000_00000000n,
|
||||
// heuristics
|
||||
coinjoin: 0b00000001_00000000_00000000_00000000_00000000n,
|
||||
consolidation: 0b00000010_00000000_00000000_00000000_00000000n,
|
||||
batch_payout: 0b00000100_00000000_00000000_00000000_00000000n,
|
||||
// sighash
|
||||
sighash_all: 0b00000001_00000000_00000000_00000000_00000000_00000000n,
|
||||
sighash_none: 0b00000010_00000000_00000000_00000000_00000000_00000000n,
|
||||
sighash_single: 0b00000100_00000000_00000000_00000000_00000000_00000000n,
|
||||
sighash_default:0b00001000_00000000_00000000_00000000_00000000_00000000n,
|
||||
sighash_acp: 0b00010000_00000000_00000000_00000000_00000000_00000000n,
|
||||
};
|
||||
|
||||
export const TransactionFilters: { [key: string]: Filter } = {
|
||||
/* features */
|
||||
rbf: { key: 'rbf', label: 'RBF enabled', flag: TransactionFlags.rbf, toggle: 'rbf', important: true },
|
||||
no_rbf: { key: 'no_rbf', label: 'RBF disabled', flag: TransactionFlags.no_rbf, toggle: 'rbf', important: true },
|
||||
v1: { key: 'v1', label: 'Version 1', flag: TransactionFlags.v1, toggle: 'version' },
|
||||
v2: { key: 'v2', label: 'Version 2', flag: TransactionFlags.v2, toggle: 'version' },
|
||||
// multisig: { key: 'multisig', label: 'Multisig', flag: TransactionFlags.multisig },
|
||||
/* address types */
|
||||
p2pk: { key: 'p2pk', label: 'P2PK', flag: TransactionFlags.p2pk, important: true },
|
||||
p2ms: { key: 'p2ms', label: 'Bare multisig', flag: TransactionFlags.p2ms, important: true },
|
||||
p2pkh: { key: 'p2pkh', label: 'P2PKH', flag: TransactionFlags.p2pkh, important: true },
|
||||
p2sh: { key: 'p2sh', label: 'P2SH', flag: TransactionFlags.p2sh, important: true },
|
||||
p2wpkh: { key: 'p2wpkh', label: 'P2WPKH', flag: TransactionFlags.p2wpkh, important: true },
|
||||
p2wsh: { key: 'p2wsh', label: 'P2WSH', flag: TransactionFlags.p2wsh, important: true },
|
||||
p2tr: { key: 'p2tr', label: 'Taproot', flag: TransactionFlags.p2tr, important: true },
|
||||
/* behavior */
|
||||
cpfp_parent: { key: 'cpfp_parent', label: 'Paid for by child', flag: TransactionFlags.cpfp_parent, important: true },
|
||||
cpfp_child: { key: 'cpfp_child', label: 'Pays for parent', flag: TransactionFlags.cpfp_child, important: true },
|
||||
replacement: { key: 'replacement', label: 'Replacement', flag: TransactionFlags.replacement, important: true },
|
||||
/* data */
|
||||
op_return: { key: 'op_return', label: 'OP_RETURN', flag: TransactionFlags.op_return, important: true },
|
||||
fake_pubkey: { key: 'fake_pubkey', label: 'Fake pubkey', flag: TransactionFlags.fake_pubkey },
|
||||
inscription: { key: 'inscription', label: 'Inscription', flag: TransactionFlags.inscription, important: true },
|
||||
/* heuristics */
|
||||
coinjoin: { key: 'coinjoin', label: 'Coinjoin', flag: TransactionFlags.coinjoin, important: true },
|
||||
consolidation: { key: 'consolidation', label: 'Consolidation', flag: TransactionFlags.consolidation },
|
||||
batch_payout: { key: 'batch_payout', label: 'Batch payment', flag: TransactionFlags.batch_payout },
|
||||
/* sighash */
|
||||
sighash_all: { key: 'sighash_all', label: 'sighash_all', flag: TransactionFlags.sighash_all },
|
||||
sighash_none: { key: 'sighash_none', label: 'sighash_none', flag: TransactionFlags.sighash_none },
|
||||
sighash_single: { key: 'sighash_single', label: 'sighash_single', flag: TransactionFlags.sighash_single },
|
||||
sighash_default: { key: 'sighash_default', label: 'sighash_default', flag: TransactionFlags.sighash_default },
|
||||
sighash_acp: { key: 'sighash_acp', label: 'sighash_anyonecanpay', flag: TransactionFlags.sighash_acp },
|
||||
};
|
||||
|
||||
export const FilterGroups: { label: string, filters: Filter[]}[] = [
|
||||
{ label: 'Features', filters: ['rbf', 'no_rbf', 'v1', 'v2', 'multisig'] },
|
||||
{ label: 'Address Types', filters: ['p2pk', 'p2ms', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr'] },
|
||||
{ label: 'Behavior', filters: ['cpfp_parent', 'cpfp_child', 'replacement'] },
|
||||
{ label: 'Data', filters: ['op_return', 'fake_pubkey', 'inscription'] },
|
||||
{ label: 'Heuristics', filters: ['coinjoin', 'consolidation', 'batch_payout'] },
|
||||
{ label: 'Sighash Flags', filters: ['sighash_all', 'sighash_none', 'sighash_single', 'sighash_default', 'sighash_acp'] },
|
||||
].map(group => ({ label: group.label, filters: group.filters.map(filter => TransactionFilters[filter] || null).filter(f => f != null) }));
|
||||
@@ -44,6 +44,7 @@ import { StartComponent } from '../components/start/start.component';
|
||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
||||
import { AddressComponent } from '../components/address/address.component';
|
||||
import { SearchFormComponent } from '../components/search-form/search-form.component';
|
||||
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
|
||||
@@ -141,6 +142,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
StartComponent,
|
||||
BlockOverviewGraphComponent,
|
||||
BlockOverviewTooltipComponent,
|
||||
BlockFiltersComponent,
|
||||
TransactionsListComponent,
|
||||
AddressComponent,
|
||||
SearchFormComponent,
|
||||
@@ -266,6 +268,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
StartComponent,
|
||||
BlockOverviewGraphComponent,
|
||||
BlockOverviewTooltipComponent,
|
||||
BlockFiltersComponent,
|
||||
TransactionsListComponent,
|
||||
AddressComponent,
|
||||
SearchFormComponent,
|
||||
@@ -319,7 +322,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
ClockFaceComponent,
|
||||
|
||||
OnlyVsizeDirective,
|
||||
OnlyWeightDirective
|
||||
OnlyWeightDirective,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
||||
BIN
frontend/src/resources/mempool-blocks-2-3-logo.jpeg
Normal file
BIN
frontend/src/resources/mempool-blocks-2-3-logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src/resources/mempool-blocks-3-2-logo.jpeg
Normal file
BIN
frontend/src/resources/mempool-blocks-3-2-logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user