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>
|
||||
|
||||
Reference in New Issue
Block a user