Merge branch 'mempool:master' into html-quickfix

This commit is contained in:
Hans ❤️ Crypto 2024-07-13 10:23:28 +02:00 committed by GitHub
commit c5ef1011d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 13955 additions and 11435 deletions

View File

@ -42,6 +42,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
@ -361,6 +362,20 @@ class BitcoinRoutes {
}
}
private async $getBlockTxAuditSummary(req: Request, res: Response) {
try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary);
} else {
return res.status(404).send(`transaction audit not available`);
}
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlocks(req: Request, res: Response) {
try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin

View File

@ -2,7 +2,7 @@ import config from '../config';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger';
import memPool from './mempool';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
import { Common } from './common';
import diskCache from './disk-cache';
import transactionUtils from './transaction-utils';
@ -1359,6 +1359,14 @@ class Blocks {
}
}
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
} else {
return null;
}
}
public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime;
}

View File

@ -333,7 +333,9 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app);
}
}
healthCheck(): void {

View File

@ -42,6 +42,19 @@ export interface BlockAudit {
matchRate: number,
expectedFees?: number,
expectedWeight?: number,
template?: any[];
}
export interface TransactionAudit {
seen?: boolean;
expected?: boolean;
added?: boolean;
prioritized?: boolean;
delayed?: number;
accelerated?: boolean;
conflict?: boolean;
coinbase?: boolean;
firstSeen?: number;
}
export interface AuditScore {

View File

@ -1,7 +1,7 @@
import blocks from '../api/blocks';
import DB from '../database';
import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces';
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
@ -98,6 +98,41 @@ class BlocksAuditRepositories {
}
}
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
try {
const blockAudit = await this.$getBlockAudit(hash);
if (blockAudit) {
const isAdded = blockAudit.addedTxs.includes(txid);
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
const isConflict = blockAudit.fullrbfTxs.includes(txid);
let isExpected = false;
let firstSeen = undefined;
blockAudit.template?.forEach(tx => {
if (tx.txid === txid) {
isExpected = true;
firstSeen = tx.time;
}
});
return {
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
}
}
return null;
} catch (e: any) {
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
try {
const [rows]: any[] = await DB.query(

View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 21, 2024.
Signed: hans-crypto

3
contributors/svrgnty.txt Normal file
View File

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 9, 2024.
Signed: svrgnty

View File

@ -40,6 +40,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false}
__SERVICES_API__=${SERVICES_API:=false}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
@ -69,6 +70,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __ACCELERATOR__
export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__

View File

@ -25,5 +25,6 @@
"HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": false,
"PUBLIC_ACCELERATIONS": false
"PUBLIC_ACCELERATIONS": false,
"SERVICES_API": "https://mempool.space/api/v1/services"
}

View File

@ -2,7 +2,7 @@
@if (accelerateError) {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;">Sorry, something went wrong!</h1>
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
</div>
</div>
<div class="row text-center mt-1">
@ -552,22 +552,15 @@
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
<ng-template #accelerateButton>
@if (!couldPay && !quoteError && !(estimate?.availablePaymentMethods.bitcoin || estimate?.availablePaymentMethods.balance)) {
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px">
<div class="position-relative">
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span>Coming soon</span>
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
</button>
} @else {
<div class="position-relative">
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
</button>
@if (quoteError || cantPayReason) {
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
}
</div>
}
@if (quoteError || cantPayReason) {
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
}
</div>
</ng-template>
<ng-template #accountPayButton>

View File

@ -8,6 +8,7 @@ import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
@ -126,7 +127,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private etaService: EtaService,
private audioService: AudioService,
private cd: ChangeDetectorRef,
private authService: AuthServiceMempool
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.accelerationUUID = window.crypto.randomUUID();
}
@ -198,6 +200,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate();
}
if (this._step === 'checkout') {
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true;
@ -292,6 +297,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.validateChoice();
if (!this.couldPay) {
this.quoteError = `cannot_accelerate_tx`;
if (this.step === 'summary') {
this.unavailable.emit(true);
}
return;
}
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice();
@ -546,7 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get couldPayWithCashapp() {
if (!this.cashappEnabled || this.stateService.referrer !== 'https://cash.app/') {
if (!this.cashappEnabled) {
return false;
}
return !!this.estimate?.availablePaymentMethods?.cashapp;
@ -569,7 +582,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
get canPayWithCashapp() {
if (!this.cashappEnabled || !this.conversions || this.stateService.referrer !== 'https://cash.app/') {
if (!this.cashappEnabled || !this.conversions) {
return false;
}

View File

@ -1,4 +1,4 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="fee-graph" *ngIf="tx && estimate" #feeGraph>
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">

View File

@ -1,20 +1,16 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar {
rate: number;
style: any;
style?: Record<string,string>;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
height?: number;
}
@Component({
@ -22,7 +18,7 @@ interface GraphBar {
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() showEstimate = false;
@ -30,13 +26,37 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
@ViewChild('feeGraph')
container: ElementRef<HTMLDivElement>;
height: number;
observer: ResizeObserver;
stopResizeLoop = false;
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
constructor(
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void {
this.initGraph();
}
ngAfterViewInit(): void {
if (ResizeObserver) {
this.observer = new ResizeObserver(entries => {
for (const entry of entries) {
this.height = entry.contentRect.height;
this.initGraph();
}
});
this.observer.observe(this.container.nativeElement);
} else {
this.startResizeFallbackLoop();
}
}
ngOnChanges(): void {
this.initGraph();
}
@ -45,44 +65,61 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
if (!this.tx || !this.estimate) {
return;
}
const hasNextBlockRate = (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee);
const numBars = hasNextBlockRate ? 4 : 3;
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: this.showEstimate ? $localize`maximum` : $localize`:@@25fbf6e80a945703c906a5a7d8c92e8729c7ab21:accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
const bars: GraphBar[] = [];
let lastHeight = 0;
if (hasNextBlockRate) {
lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
height: lastHeight,
class: 'target',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
}
this.maxRateOptions.forEach((option, index) => {
lastHeight = Math.max(lastHeight + 30, (this.height * ((option.rate - baseRate) / maxRate)));
bars.push({
rate: option.rate,
height: lastHeight,
class: 'max',
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
})
})
bars.reverse();
baseHeight = this.height - lastHeight;
for (const bar of bars) {
bar.style = this.getStyle(bar.height, baseHeight);
}
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
style: this.getStyle(baseHeight, 0),
height: baseHeight,
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
this.cd.detectChanges();
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
getStyle(height: number, base: number): Record<string,string> {
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
height: `${height}px`,
bottom: base ? `${base}px` : '0',
}
}
@ -96,4 +133,20 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
startResizeFallbackLoop(): void {
if (this.stopResizeLoop) {
return;
}
requestAnimationFrame(() => {
this.height = this.container?.nativeElement?.clientHeight || 0;
this.initGraph();
this.startResizeFallbackLoop();
});
}
ngOnDestroy(): void {
this.stopResizeLoop = true;
this.observer.disconnect();
}
}

View File

@ -1,3 +1,4 @@
@if (tx.status.confirmed) {
<div class="acceleration-timeline box">
<div class="timeline-wrapper">
<div class="timeline">
@ -11,68 +12,141 @@
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time kind="plain" [time]="eta?.wait / 1000"></app-time>
} @else if (tx.status.block_time) {
<app-time kind="plain" [time]="tx.status.block_time - acceleratedAt"></app-time>
}
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
</div>
</div>
<div class="node-spacer"></div>
</div>
</div>
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !tx.acceleration">
<div class="shape"></div>
</a>
<div class="status"><span class="badge badge-primary">Sent</span></div>
<div class="time">
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
<div class="time">
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
</div>
</div>
</div>
<div class="interval-spacer">
<div class="seen-to-acc" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'accelerated'">
<div class="seen-to-acc left" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
<div class="acc-to-confirmed right" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.accelerated-selected]="tx.acceleration && !tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.acceleration && !tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-accelerated">Accelerated</span></div>
<div class="time">
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
<div class="interval-spacer">
<div class="seen-to-acc"></div>
</div>
</div>
<div class="interval-spacer">
<div class="acc-to-confirmed" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed">
<div class="acc-to-confirmed left" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.mined-selected]="tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-success">Mined</span></div>
<div class="time">
@if (tx.status.block_time) {
<div class="node" [id]="'accelerated'">
<div class="seen-to-acc left"></div>
<div class="acc-to-confirmed right"></div>
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
<div class="time">
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time>
</div>
</div>
<div class="interval-spacer">
<div class="acc-to-confirmed"></div>
</div>
<div class="node selected" [id]="'confirmed'">
<div class="acc-to-confirmed left" ></div>
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
<div class="time">
<app-time kind="since" [time]="tx.status.block_time"></app-time>
} @else if (eta) {
<app-time kind="until" [time]="eta?.time"></app-time>
}
</div>
</div>
</div>
</div>
</div>
</div>
} @else if (acceleratedETA) { <!-- Not yet accelerated; to be shown only in acceleration checkout -->
} @else if (standardETA) { <!-- Accelerated -->
<div class="acceleration-timeline box lower-padding">
<div class="timeline-wrapper">
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
<div class="interval-spacer"></div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
}
</div>
</div>
<div class="node-spacer"></div>
</div>
<div class="nodes">
<div class="node-spacer"></div>
<div class="interval-spacer"></div>
<div class="node">
<div class="acc-to-confirmed right go-faster"></div>
</div>
<div class="interval-spacer">
</div>
<div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div>
<div class="shape-border waiting">
<div class="shape animate"></div>
</div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
</div>
</div>
</div>
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<app-time [time]="acceleratedAt - transactionTime"></app-time>
</div>
</div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
-
</div>
</div>
<div class="node-spacer"></div>
</div>
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border">
<div class="shape"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
<div class="time">
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
</div>
</div>
<div class="interval-spacer">
<div class="seen-to-acc"></div>
</div>
<div class="node accelerated" [id]="'accelerated'">
<div class="seen-to-acc left"></div>
<div class="seen-to-acc right"></div>
<div class="shape-border">
<div class="shape"></div>
<div class="connector down loading"></div>
</div>
<div class="time" style="margin-top: 3px;">
<span i18n="transaction.audit.accelerated">Accelerated</span>&nbsp;<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
</div>
</div>
<div class="interval-spacer">
<div class="seen-to-acc"></div>
</div>
<div class="node" [id]="'confirmed'">
<div class="seen-to-acc left"></div>
<div class="shape-border waiting">
<div class="shape"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #nodeSpacer>
<div class="node-spacer"></div>
</ng-template>
<ng-template #intervalSpacer>
<div class="interval-spacer"></div>
</ng-template>
</div>
}

View File

@ -2,6 +2,9 @@
position: relative;
width: 100%;
padding: 1em 0;
&.lower-padding {
padding: 0.5em 0 1em;
}
&::after, &::before {
content: '';
@ -52,7 +55,7 @@
.interval, .interval-spacer {
width: 8em;
min-width: 5em;
min-width: 8em;
max-width: 8em;
height: 32px;
display: flex;
@ -69,6 +72,15 @@
font-size: 12px;
line-height: 16px;
white-space: nowrap;
.compare {
font-style: italic;
color: var(--mainnet-alt);
font-weight: 600;
@media (max-width: 600px) {
display: none;
}
}
}
}
@ -84,10 +96,6 @@
background: var(--primary);
border-radius: 5px;
&.loading {
animation: standardPulse 1s infinite;
}
&.left {
right: 50%;
}
@ -107,8 +115,20 @@
background: var(--tertiary);
border-radius: 5px;
&.loading {
animation: acceleratePulse 1s infinite;
&.go-faster {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath style='fill:%239339f4;' d='M 0,0 5,5 0,10 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,0 10,0 15,5 10,10 0,10 5,5 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 10,0 20,0 20,10 10,10 15,5 Z'/%3E%3C/svg%3E%0A"); background-size: 20px 10px;
border-radius: 0;
&.right {
left: calc(50% + 5px);
margin-right: calc(-4em + 5px);
animation: goFasterRight 0.8s infinite linear;
}
&.left {
right: calc(50% + 5px);
margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear;
}
}
&.left {
@ -118,7 +138,6 @@
left: 50%;
}
}
}
.nodes {
@ -133,39 +152,70 @@
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
padding: 2px;
cursor: pointer;
padding: 4px;
background: transparent;
transition: background-color 300ms, padding 300ms;
.shape {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: white;
transition: background-color 300ms, border 300ms;
z-index: 1;
}
&.sent-selected {
&.waiting {
.shape {
background: var(--primary);
background: var(--grey);
}
}
&.accelerated-selected {
.shape {
background: var(--tertiary);
.connector {
position: absolute;
z-index: 0;
height: 88px;
width: 10px;
left: -5px;
top: -73px;
transform: translateX(120%);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='20'%3E%3Cpath style='fill:%239339f4;' d='M 0,20 5,15 10,20 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,20 5,15 10,20 10,10 5,5 0,10 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 0,10 5,5 10,10 10,0 0,0 Z'/%3E%3C/svg%3E%0A"); // linear-gradient(135deg, var(--tertiary) 34%, transparent 34%),
background-size: 10px 20px;
&.down {
border-top-left-radius: 10px;
}
&.up {
border-top-right-radius: 10px;
}
&.loading {
animation: goFasterUp 0.8s infinite linear;
}
}
}
&.accelerated {
.shape-border {
animation: acceleratePulse 0.4s infinite;
}
}
&.mined-selected {
.shape {
background: var(--success);
}
&.selected {
.shape-border {
background: var(--mainnet-alt);
}
}
.status {
margin-top: -64px;
.badge.badge-waiting {
opacity: 0.5;
background-color: var(--grey);
color: white;
}
.badge.badge-accelerated {
background-color: var(--tertiary);
@ -189,9 +239,17 @@
100% { background-color: var(--tertiary) }
}
@keyframes standardPulse {
0% { background-color: var(--primary) }
50% { background-color: var(--secondary) }
100% { background-color: var(--primary) }
}
@keyframes goFasterUp {
0% { background-position-y: 0; }
100% { background-position-y: -40px; }
}
@keyframes goFasterLeft {
0% { background-position: left 0px bottom 0px }
100% { background-position: left 40px bottom 0px; }
}
@keyframes goFasterRight {
0% { background-position: right 0 bottom 0px; }
100% { background-position: right -40px bottom 0px; }
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
import { Component, Input, OnInit, OnChanges } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
@ -11,23 +11,33 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
@Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
dir: 'rtl' | 'ltr' = 'ltr';
now: number;
accelerateRatio: number;
constructor(
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
constructor() {}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
}
ngOnChanges(changes): void {
this.now = Math.floor(new Date().getTime() / 1000);
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
}
}

View File

@ -45,8 +45,8 @@
</form>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>

View File

@ -32,7 +32,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
@Input() height: number = 300;
@Input() right: number | string = 45;
@Input() left: number | string = 75;
@Input() period: '3d' | '1w' | '1m' = '1w';
@Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
@Input() accelerations$: Observable<Acceleration[]>;
miningWindowPreference: string;
@ -48,7 +48,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
isLoading = true;
formatNumber = formatNumber;
timespan = '';
periodSubject$: Subject<'3d' | '1w' | '1m'> = new Subject();
periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject();
chartInstance: any = undefined;
daysAvailable: number = 0;
@ -78,7 +78,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route.fragment.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});

View File

@ -4,7 +4,7 @@
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text">
<div>{{ stats.totalRequested }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
<div class="symbol" i18n="accelerator.total-accelerated-plural">accelerated</div>
</div>
</div>
<div class="item">

View File

@ -16,7 +16,7 @@ export type AccelerationStats = {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccelerationStatsComponent implements OnInit, OnChanges {
@Input() timespan: '3d' | '1w' | '1m' = '1w';
@Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
accelerationStats$: Observable<AccelerationStats>;
blocksInPeriod: number = 7 * 144;
@ -35,6 +35,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
updateStats(): void {
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$({ timeframe: this.timespan });
switch (this.timespan) {
case '24h':
this.blocksInPeriod = 144;
break;
case '3d':
this.blocksInPeriod = 3 * 144;
break;
@ -44,6 +47,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
case '1m':
this.blocksInPeriod = 30 * 144;
break;
case 'all':
this.blocksInPeriod = Infinity;
break;
}
}
}

View File

@ -23,12 +23,18 @@
<div class="main-title">
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>&nbsp;
@switch (timespan) {
@case ('24h') {
<span style="font-size: xx-small" i18n="mining.1-day">(1 day)</span>
}
@case ('1w') {
<span style="font-size: xx-small" i18n="mining.1-week">(1 week)</span>
}
@case ('1m') {
<span style="font-size: xx-small" i18n="mining.1-month">(1 month)</span>
}
@case ('all') {
<span style="font-size: xx-small" i18n="mining.all-time">(all time)</span>
}
}
</div>
<div class="card-wrapper">
@ -36,11 +42,17 @@
<div class="card-body more-padding">
<app-acceleration-stats [timespan]="timespan"></app-acceleration-stats>
<div class="widget-toggler">
<a href="" (click)="setTimespan('24h')" class="toggler-option"
[ngClass]="{'inactive': timespan === '24h'}"><small>24h</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('1w')" class="toggler-option"
[ngClass]="{'inactive': timespan === '1w'}"><small>1w</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('1m')" class="toggler-option"
[ngClass]="{'inactive': timespan === '1m'}"><small>1m</small></a>
<span style="color: #ffffff66; font-size: 8px"> | </span>
<a href="" (click)="setTimespan('all')" class="toggler-option"
[ngClass]="{'inactive': timespan === 'all'}"><small>all</small></a>
</div>
</div>
</div>

View File

@ -37,7 +37,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
webGlEnabled = true;
seen: Set<string> = new Set();
firstLoad = true;
timespan: '3d' | '1w' | '1m' = '1w';
timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
accelerationDeltaSubscription: Subscription;
@ -99,7 +99,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(),
switchMap(() => {
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe(
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed_provisional,completed', pageLength: 6 }).pipe(
catchError(() => {
return of([]);
}),

View File

@ -33,6 +33,7 @@
.menu-toggle {
width: 3em;
min-width: 3em;
height: 1.8em;
padding: 0px 1px;
opacity: 0;
@ -42,6 +43,7 @@
border: none;
border-radius: 0.35em;
pointer-events: all;
align-self: normal;
}
.filter-menu {

View File

@ -181,8 +181,8 @@
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.name }}
</a>
</td>
@ -411,7 +411,7 @@
<td class="text-wrap">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span *ngIf="oobFees" class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band">
<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true" [addPlus]="true"></app-amount>
<app-amount [satoshis]="oobFees" digitsInfo="1.8-8" [noFiat]="true" [addPlus]="true"></app-amount>
</span>
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%

View File

@ -272,3 +272,11 @@ h1 {
}
}
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 2px;
}

View File

@ -59,10 +59,11 @@
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
</ng-container>
</div>
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined && showPools">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
{{ block.extras.pool.name}}</a>
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
{{ block.extras.pool.name}}
</a>
</div>
</div>
</ng-container>
@ -85,7 +86,7 @@
</ng-template>
</div>
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition"
[ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
[ngStyle]="{'left': arrowLeftPx + 8 + 'px' }"></div>
</div>
<ng-template #loadingBlocksTemplate>

View File

@ -125,12 +125,12 @@
#arrow-up {
position: relative;
left: calc(var(--block-size) * 0.6);
top: calc(var(--block-size) * 1.12);
top: calc(var(--block-size) * 1.28);
width: 0;
height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent;
border-right: calc(var(--block-size) * 0.3) solid transparent;
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
border-left: calc(var(--block-size) * 0.2) solid transparent;
border-right: calc(var(--block-size) * 0.2) solid transparent;
border-bottom: calc(var(--block-size) * 0.2) solid var(--fg);
}
.flashing {
@ -157,17 +157,20 @@
position: relative;
top: 15px;
z-index: 101;
color: #FFF;
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 2px;
}
.animated {
transition: all 0.15s ease-in-out;
}
.show {
opacity: 1;
}
.hide {
opacity: 0.4;
pointer-events : none;
white-space: nowrap;
}
.time-ltr {

View File

@ -14,8 +14,7 @@
}
.blockchain-wrapper {
height: 250px;
height: 260px;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+/Edge */
@ -57,7 +56,7 @@
color: var(--fg);
font-size: 0.8rem;
position: absolute;
bottom: -1.8em;
bottom: -2.2em;
left: 1px;
transform: translateX(-50%);
background: none;

View File

@ -114,7 +114,7 @@
#arrow-up {
position: relative;
right: calc(var(--block-size) * 0.6);
top: calc(var(--block-size) * 1.12);
top: calc(var(--block-size) * 1.20);
width: 0;
height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent;

View File

@ -34,7 +34,12 @@
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">
{{ item.title }}
@if (item.isExternal === true) {
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="margin-left: 5px; font-size: 13px; color: lightgray"></fa-icon>
}
</a>
</li>
</ul>
</div>

View File

@ -5,29 +5,33 @@
<br><br>
<h2>Privacy Policy</h2>
<h6>Updated: November 23, 2023</h6>
<h6>Updated: July 10, 2024</h6>
<br><br>
<div class="text-left">
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<h4>USE YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project&reg;; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website. By using your own self-hosted instance you will have maximum security, privacy and freedom.</p>
<br>
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
<br>
<h5>By accessing this Website, you agree to the following Privacy Policy:</h5>
<br>
<h4>TRUSTED THIRD PARTIES ARE SECURITY HOLES</h4>
<h4>General</h4>
<p>Out of respect for the Bitcoin community, this website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this website.</p>
<p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p>
<br>
<h4>TRUSTED FIRST PARTIES ARE ALSO SECURITY HOLES</h4>
<p>Out of respect for the Bitcoin community, this website does not use any first-party cookies, except to store your preferred language setting (if any). However, we do use minimal first-party analytics and logging as needed for the operation of this website, as follows:</p>
<p>Out of respect for the Bitcoin community, this Website does not use any first-party cookies, except to store your preferred language setting (if any). However, we do use minimal first-party analytics and logging as needed for the operation of this Website, as follows:</p>
<ul>
@ -41,35 +45,49 @@
<br>
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
<h4>USING MEMPOOL ACCELERATOR&trade;</h4>
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project&reg; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&trade; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="!officialMempoolSpace">If you click the accelerate button on a transaction you will load acceleration pricing information from Mempool. If you make an acceleration request, the TXID and your maximum bid will be sent to Mempool who will store and share this information with their mining pool partners, and publicly display accelerated transaction details on mempool.space and via Mempool's APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<br>
<ng-container *ngIf="officialMempoolSpace">
<h4>DONATING TO MEMPOOL.SPACE</h4>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<p>If you donate to mempool.space, your payment information and your Twitter identity (if provided) will be collected in a database, which may be used to publicly display the sponsor profiles on <a href="https://mempool.space/about">mempool.space/about</a>. Thank you for supporting The Mempool Open Source Project.</p>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<br>
<ul>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor (see "Payments" below).</li>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<li>If you connect your X (fka Twitter) account, we may store your X identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project&reg;, claim your Lightning node, or other such use cases.</li>
<ol>
<li>If you sign up for a subscription to Mempool Enterprise&trade; we also collect your company name which is not shared with any third-party.</li>
<li>If you provide your name, country, and/or e-mail address, we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
<li>If you sign up for an account on mempool.space and use Mempool Accelerator&trade; Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
</ul>
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
<br>
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
</ng-container>
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
<h4>PAYMENTS AND DONATIONS</h4>
</ol>
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project&reg;, we may collect the following:</p>
<ul>
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor.</li>
<li>If you make a payment using Bitcoin, we will process your payment using our self-hosted BTCPay Server instance. We will not share your payment details with any third-party. For payments made over the Lightning network, we may utilize third party LSPs / lightning liquidity providers.</li>
<li>If you make a payment using Fiat we will collect your payment details. We will share your payment details with our fiat payment processor Square (Block, Inc.),. - Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy.</li>
</ul>
<br>

View File

@ -47,6 +47,7 @@
<div class="card-title" i18n="search.mining-pools">Mining Pools</div>
<ng-template ngFor [ngForOf]="results.pools" let-pool let-i="index">
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [class.inactive]="!pool.active" type="button" role="option" class="dropdown-item">
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
<ngb-highlight [result]="pool.name" [term]="results.searchText"></ngb-highlight>
</button>
</ng-template>

View File

@ -26,3 +26,11 @@
.active {
background-color: var(--active-bg);
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 10px;
}

View File

@ -5,7 +5,7 @@
<br /><br />
<h2>Terms of Service</h2>
<h6>Updated: August 02, 2021</h6>
<h6>Updated: July 10, 2024</h6>
<br><br>
@ -67,6 +67,38 @@
</ng-container>
<h4>MEMPOOL ACCELERATOR&trade;</h4>
<p><a href="https://mempool.space/accelerator">Mempool Accelerator&trade;</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
<ul>
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
<br>
<li>Acceleration requests cannot be canceled by the user once submitted. </li>
<br>
<li>Mempool reserves the right to cancel acceleration requests for any reason, including but not limited to the ejection of an accelerated transaction from Mempool's mempool. Canceled accelerations will not be refunded.</li>
<br>
<li>All acceleration payments and Mempool Accelerator&trade; account credit top-ups are non-refundable. </li>
<br>
<li>Mempool Accelerator&trade; account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
<br>
<li>Mempool does not provide acceleration services to persons in Cuba, Iran, North Korea, Russia, Syria, Crimea, Donetsk or Luhansk Regions of Ukraine.</li>
</ul>
<br>
<p>EOF</p>
</div>

View File

@ -35,6 +35,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second';
@Input() fractionDigits: number = 0;
@Input() lowercaseStart = false;
constructor(
private ref: ChangeDetectorRef,
@ -106,6 +107,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
return $localize`:@@date-base.immediately:Immediately`;
} else if (seconds < 60) {
if (this.relative || this.kind === 'since') {
if (this.lowercaseStart) {
return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1);
}
return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until' || this.kind === 'within') {
seconds = 60;

View File

@ -75,9 +75,6 @@
} @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
}
<!-- @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
} -->
</span>
</ng-container>
<ng-template #etaSkeleton>

View File

@ -728,7 +728,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
if (!this.txId) {
return;
}
this.enterpriseService.goal(8);
this.accelerationFlowCompleted = false;
if (this.showAccelerationSummary) {
this.scrollIntoAccelPreview = true;

View File

@ -153,15 +153,6 @@
<br>
<ng-container *ngIf="transactionTime && (tx.acceleration || isAcceleration)">
<div class="title float-left">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div>
<div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)"></app-acceleration-timeline>
<br>
</ng-container>
<ng-container *ngIf="rbfInfo">
<div class="title float-left">
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
@ -171,6 +162,15 @@
<br>
</ng-container>
<ng-container *ngIf="transactionTime && isAcceleration">
<div class="title float-left">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div>
<div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
<br>
</ng-container>
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
@ -449,7 +449,7 @@
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div>
@ -639,7 +639,7 @@
}
<td>
<div class="effective-fee-container">
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
@ -676,9 +676,9 @@
<td class="td-width" i18n="block.miner">Miner</td>
@if (pool) {
<td class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
{{ pool.name }}
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
{{ pool.name }}
</a>
</td>
} @else {

View File

@ -324,4 +324,12 @@
.goggles-icon {
display: block;
width: 2.7em;
}
}
.pool-logo {
width: 15px;
height: 15px;
position: relative;
top: -1px;
margin-right: 2px;
}

View File

@ -42,7 +42,7 @@ interface Pool {
slug: string;
}
interface AuditStatus {
export interface TxAuditStatus {
seen?: boolean;
expected?: boolean;
added?: boolean;
@ -65,6 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txId: string;
txInBlockIndex: number;
mempoolPosition: MempoolPosition;
gotInitialPosition = false;
accelerationPositions: AccelerationPosition[];
isLoadingTx = true;
error: any = undefined;
@ -99,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
sigops: number | null;
adjustedVsize: number | null;
pool: Pool | null;
auditStatus: AuditStatus | null;
auditStatus: TxAuditStatus | null;
isAcceleration: boolean = false;
filters: Filter[] = [];
showCpfpDetails = false;
@ -112,6 +113,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
ETA$: Observable<ETA | null>;
standardETA$: Observable<ETA | null>;
isCached: boolean = false;
now = Date.now();
da$: Observable<DifficultyAdjustment>;
@ -130,6 +132,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
tooltipPosition: { x: number, y: number };
isMobile: boolean;
firstLoad = true;
waitingForAccelerationInfo: boolean = false;
featuresEnabled: boolean;
segwitEnabled: boolean;
@ -315,11 +318,19 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setIsAccelerated();
}),
switchMap((blockHeight: number) => {
return this.servicesApiService.getAccelerationHistory$({ blockHeight });
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
switchMap((accelerationHistory: Acceleration[]) => {
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
return throwError('retry');
}
return of(accelerationHistory);
}),
retry({ count: 3, delay: 2000 }),
catchError(() => {
return of([]);
})
);
}),
catchError(() => {
return of([]);
})
).subscribe((accelerationHistory) => {
for (const acceleration of accelerationHistory) {
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
@ -328,13 +339,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
acceleration.boost = boostCost;
this.tx.acceleratedAt = acceleration.added;
this.accelerationInfo = acceleration;
this.waitingForAccelerationInfo = false;
this.setIsAccelerated();
}
}
});
this.miningSubscription = this.fetchMiningInfo$.pipe(
filter((target) => target.txid === this.txId),
filter((target) => target.txid === this.txId && !this.pool),
tap(() => {
this.pool = null;
}),
@ -362,33 +374,41 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
const auditAvailable = this.isAuditAvailable(height);
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
const fetchAudit = auditAvailable && !isCoinbase;
return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
map(audit => {
const isAdded = audit.addedTxs.includes(txid);
const isPrioritized = audit.prioritizedTxs.includes(txid);
const isAccelerated = audit.acceleratedTxs.includes(txid);
const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid);
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
return {
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
};
}),
retry({ count: 3, delay: 2000 }),
catchError(() => {
return of(null);
})
) : of(isCoinbase ? { coinbase: true } : null);
if (fetchAudit) {
// If block audit is already cached, use it to get transaction audit
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
if (blockAuditLoaded) {
return this.apiService.getBlockAudit$(hash).pipe(
map(audit => {
const isAdded = audit.addedTxs.includes(txid);
const isPrioritized = audit.prioritizedTxs.includes(txid);
const isAccelerated = audit.acceleratedTxs.includes(txid);
const isConflict = audit.fullrbfTxs.includes(txid);
const isExpected = audit.template.some(tx => tx.txid === txid);
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
return {
seen: isExpected || isPrioritized || isAccelerated,
expected: isExpected,
added: isAdded,
prioritized: isPrioritized,
conflict: isConflict,
accelerated: isAccelerated,
firstSeen,
};
})
)
} else {
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
retry({ count: 3, delay: 2000 }),
catchError(() => {
return of(null);
})
)
}
} else {
return of(isCoinbase ? { coinbase: true } : null);
}
}),
catchError((e) => {
return of(null);
})
).subscribe(auditStatus => {
this.auditStatus = auditStatus;
if (this.auditStatus?.firstSeen) {
@ -431,9 +451,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
this.cashappEligible = true;
}
if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
this.accelerationFlowCompleted = true;
}
}
}
}
this.gotInitialPosition = true;
} else {
this.mempoolPosition = null;
this.accelerationPositions = null;
@ -602,12 +626,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
if (this.tx.acceleration) {
this.waitingForAccelerationInfo = true;
}
this.tx.status = {
confirmed: true,
block_height: block.height,
block_hash: block.id,
block_time: block.timestamp,
};
this.pool = block.extras.pool;
this.txChanged$.next(true);
this.stateService.markBlock$.next({ blockHeight: block.height });
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
@ -718,7 +746,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
document.location.hash = '#accelerate';
this.enterpriseService.goal(8);
this.openAccelerator();
this.scrollIntoAccelPreview = true;
return false;
@ -797,7 +824,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
if (this.isAcceleration) {
if (initialState) {
this.accelerationFlowCompleted = true;
@ -809,6 +836,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = stats;
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
});
if (!this.tx.status?.confirmed) {
this.standardETA$ = combineLatest([
this.stateService.mempoolBlocks$.pipe(startWith(null)),
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
]).pipe(
map(([mempoolBlocks, da]) => {
return this.etaService.calculateUnacceleratedETA(
this.tx,
mempoolBlocks,
da,
this.cpfpInfo,
);
})
)
}
}
this.isAccelerated$.next(this.isAcceleration);
}
@ -864,6 +906,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
resetTransaction() {
this.firstLoad = false;
this.gotInitialPosition = false;
this.error = undefined;
this.tx = null;
this.txChanged$.next(true);

View File

@ -6,7 +6,7 @@
<app-truncate [text]="tx.txid"></app-truncate>
</a>
<div>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
</ng-template>

View File

@ -9017,18 +9017,89 @@ export const restApiDocsData = [
"effectiveFee": 154,
"ancestorCount": 1
},
"cost": 3850,
"targetFeeRate": 26,
"nextBlockFee": 4004,
"userBalance": 99900000,
"mempoolBaseFee": 40000,
"vsizeFee": 50000,
"hasAccess": true
"cost": 1386,
"targetFeeRate": 10,
"nextBlockFee": 1540,
"userBalance": 0,
"mempoolBaseFee": 50000,
"vsizeFee": 0,
"pools": [
111,
102,
112,
142,
115
],
"options": [
{
"fee": 1500
},
{
"fee": 3000
},
{
"fee": 12500
}
],
"hasAccess": false,
"availablePaymentMethods": {
"bitcoin": {
"enabled": true,
"min": 1000,
"max": 10000000
},
"cashapp": {
"enabled": true,
"min": 10,
"max": 200
}
},
"unavailable": false
}`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
category: "accelerator-public",
httpRequestMethod: "POST",
fragment: "accelerator-get-invoice",
title: "POST Generate Acceleration Invoice",
description: {
default: "<p>Request a LN invoice to accelerate a transaction.</p>"
},
urlString: "/v1/services/payments/bitcoin",
showConditions: [""],
showJsExamples: showJsExamplesDefaultFalse,
codeExample: {
default: {
codeTemplate: {
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/payments/bitcoin`, //custom interpolation technique handled in replaceCurlPlaceholder()
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ["product=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&amount=12500"],
headers: "",
response: `[
{
"btcpayInvoiceId": "4Ww53d7VgSa596jmCFufe7",
"btcDue": "0.000625",
"addresses": {
"BTC": "bc1qcvqx2kr5mktd7gvym0atrrx0sn27mwv5kkghl3m78kegndm5t8ksvcqpja",
"BTC_LNURLPAY": null,
"BTC_LightningLike": "lnbc625u1pngl0wzpp56j7cqghsw2y5q7vdu9shmpxgpzsx4pqra4wcm9vdnvqegutplk2qdxj2pskjepqw3hjqnt9d4cx7mmvypqkxcm9d3jhyct5daezq2z0wfjx2u3qf9zr5grpvd3k2mr9wfshg6t0dckk2ef3xdjkyc3e8ymrxv3nxumkxvf4vvungwfcxqen2dmxxcmngepj8q6kzce5xyengdfjxq6nqvpnx9jkzdnyvdjrxefevgexgcej8yknzdejxqmrjd3jx5mrgdpj9ycqzpuxqrpr5sp58593dzj2uauaj3afa7x47qeam8k9yyqrh9qasj2ssdzstew6qv3q9qxpqysgqj8qshfkxmj0gfkly5xfydysvsx55uhnc6fgpw66uf6hl8leu07454axe2kq0q788yysg8guel2r36d6f75546nkhmdcmec4mmlft8dsq62rnsj"
}
}
]`,
},
}
}
},
{
options: { officialOnly: true },
type: "endpoint",
@ -9119,14 +9190,18 @@ export const restApiDocsData = [
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
"status": "completed",
"added": 1707421053,
"lastUpdated": 1707422952,
"lastUpdated": 1719134667,
"effectiveFee": 146,
"effectiveVsize": 141,
"feeDelta": 14000,
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
"blockHeight": 829559,
"bidBoost": 6102,
"pools": [111]
"bidBoost": 3239,
"boostVersion": "v1",
"pools": [
111
],
"minedByPoolUniqueId": 111
}
]`,
},
@ -9229,7 +9304,7 @@ export const restApiDocsData = [
category: "accelerator-private",
httpRequestMethod: "POST",
fragment: "accelerator-accelerate",
title: "POST Accelerate A Transaction",
title: "POST Accelerate A Transaction (Pro)",
description: {
default: "<p>Sends a request to accelerate a transaction.</p>"
},

View File

@ -194,7 +194,7 @@
</ng-template>
<ng-template type="how-to-get-transaction-confirmed-quickly">
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p *ngIf="officialMempoolInstance">Another option to get your transaction confirmed more quickly is Mempool Accelerator™. This service is still in development, but you can <a href="https://mempool.space/accelerator">sign up for the waitlist</a> to be notified when it's ready.</p>
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p>Another option to get your transaction confirmed more quickly is <a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">Mempool Accelerator™</a>.</p>
</ng-template>
<ng-template type="how-prevent-stuck-transaction">

View File

@ -33,6 +33,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
showMobileEnterpriseUpsell: boolean = true;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
@ViewChildren(FaqTemplateDirective) faqTemplates: QueryList<FaqTemplateDirective>;
dict = {};

View File

@ -50,7 +50,7 @@ const routes: Routes = [
},
{
path: 'acceleration',
data: { networks: ['bitcoin'] },
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
component: StartComponent,
children: [
{
@ -61,7 +61,7 @@ const routes: Routes = [
},
{
path: 'acceleration/list/:page',
data: { networks: ['bitcoin'] },
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
component: AccelerationsListComponent,
},
{
@ -140,7 +140,7 @@ const routes: Routes = [
},
{
path: 'acceleration/fees',
data: { networks: ['bitcoin'] },
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
component: AccelerationFeesGraphComponent,
},
{

View File

@ -411,7 +411,7 @@ export interface Acceleration {
}
export interface AccelerationHistoryParams {
status?: string;
status?: string; // Single status or comma separated list of status
timeframe?: string;
poolUniqueId?: number;
blockHash?: string;

View File

@ -5,6 +5,7 @@ export type MenuItem = {
i18n: string;
faIcon: IconName;
link: string;
isExternal?: boolean;
};
export type MenuGroup = {
title: string;

View File

@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface';
import { Conversion } from './price.service';
import { StorageService } from './storage.service';
import { WebsocketResponse } from '../interfaces/websocket.interface';
import { TxAuditStatus } from '../components/transaction/transaction.component';
@Injectable({
providedIn: 'root'
@ -17,6 +18,7 @@ export class ApiService {
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
public blockAuditLoaded: { [hash: string]: boolean } = {};
constructor(
private httpClient: HttpClient,
@ -369,11 +371,18 @@ export class ApiService {
}
getBlockAudit$(hash: string) : Observable<BlockAudit> {
this.setBlockAuditLoaded(hash);
return this.httpClient.get<BlockAudit>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
);
}
getBlockTxAudit$(hash: string, txid: string) : Observable<TxAuditStatus> {
return this.httpClient.get<TxAuditStatus>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/tx/${txid}/audit`
);
}
getBlockAuditScores$(from: number): Observable<AuditScore[]> {
return this.httpClient.get<AuditScore[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
@ -526,4 +535,13 @@ export class ApiService {
this.apiBaseUrl + this.apiBasePath + '/api/v1/accelerations/total' + (queryString?.length ? '?' + queryString : '')
);
}
// Cache methods
async setBlockAuditLoaded(hash: string) {
this.blockAuditLoaded[hash] = true;
}
getBlockAuditLoaded(hash) {
return this.blockAuditLoaded[hash];
}
}

View File

@ -124,6 +124,7 @@ export class CacheService {
resetBlockCache() {
this.blockHashCache = {};
this.blockCache = {};
this.apiService.blockAuditLoaded = {};
this.blockLoading = {};
this.copiesInBlockQueue = {};
this.blockPriorities = [];

View File

@ -225,4 +225,58 @@ export class EtaService {
blocks: Math.ceil(eta / da.adjustedTimeAvg),
};
}
calculateUnacceleratedETA(
tx: Transaction,
mempoolBlocks: MempoolBlock[],
da: DifficultyAdjustment,
cpfpInfo: CpfpInfo | null,
): ETA | null {
if (!tx || !mempoolBlocks) {
return null;
}
const now = Date.now();
// use known projected position, or fall back to feerate-based estimate
const mempoolPosition = this.mempoolPositionFromFees(this.getFeeRateFromCpfpInfo(tx, cpfpInfo), mempoolBlocks);
if (!mempoolPosition) {
return null;
}
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
if (!da) {
return null;
}
const blocks = mempoolPosition.block + 1;
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
return {
now,
time: wait + now + da.timeOffset,
wait,
blocks,
};
}
getFeeRateFromCpfpInfo(tx: Transaction, cpfpInfo: CpfpInfo | null): number {
if (!cpfpInfo) {
return tx.fee / (tx.weight / 4);
}
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
if (!!relatives.length) {
const totalWeight = tx.weight + relatives.reduce((prev, val) => prev + val.weight, 0);
const totalFees = tx.fee + relatives.reduce((prev, val) => prev + val.fee, 0);
return totalFees / (totalWeight / 4);
}
return tx.fee / (tx.weight / 4);
}
}

View File

@ -3,6 +3,7 @@ import { Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { StateService } from './state.service';
import { RelativeUrlPipe } from '../shared/pipes/relative-url/relative-url.pipe';
@Injectable({
providedIn: 'root'
@ -30,15 +31,30 @@ export class NavigationService {
constructor(
private stateService: StateService,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.router.routerState.snapshot.root),
).subscribe((state) => {
this.updateSubnetPaths(state);
if (this.enforceSubnetRestrictions(state)) {
this.updateSubnetPaths(state);
}
});
}
enforceSubnetRestrictions(root: ActivatedRouteSnapshot): boolean {
let route = root;
while (route) {
if (route.data.onlySubnet && !route.data.onlySubnet.includes(this.stateService.network)) {
this.router.navigate([this.relativeUrlPipe.transform('')]);
return false;
}
route = route.firstChild;
}
return true;
}
// For each network (bitcoin/liquid), find and save the longest url path compatible with the current route
updateSubnetPaths(root: ActivatedRouteSnapshot): void {
let path = '';

View File

@ -25,9 +25,6 @@ export interface IUser {
ogRank: number | null;
}
// Todo - move to config.json
const SERVICES_API_PREFIX = `/api/v1/services`;
@Injectable({
providedIn: 'root'
})
@ -98,7 +95,7 @@ export class ServicesApiServices {
return of(null);
}
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/account`);
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/account`);
}
getUserMenuGroups$(): Observable<MenuGroup[]> {
@ -107,7 +104,7 @@ export class ServicesApiServices {
return of(null);
}
return this.httpClient.get<MenuGroup[]>(`${SERVICES_API_PREFIX}/account/menu`);
return this.httpClient.get<MenuGroup[]>(`${this.stateService.env.SERVICES_API}/account/menu`);
}
logout$(): Observable<any> {
@ -117,59 +114,59 @@ export class ServicesApiServices {
}
localStorage.removeItem('auth');
return this.httpClient.post(`${SERVICES_API_PREFIX}/auth/logout`, {});
return this.httpClient.post(`${this.stateService.env.SERVICES_API}/auth/logout`, {});
}
getJWT$() {
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/auth/getJWT`);
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/auth/getJWT`);
}
getServicesBackendInfo$(): Observable<IBackendInfo> {
return this.httpClient.get<IBackendInfo>(`${SERVICES_API_PREFIX}/version`);
return this.httpClient.get<IBackendInfo>(`${this.stateService.env.SERVICES_API}/version`);
}
estimate$(txInput: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
}
accelerate$(txInput: string, userBid: number, accelerationUUID: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
}
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) {
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
}
getAccelerations$(): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
}
getAggregatedAccelerationHistory$(params: AccelerationHistoryParams): Observable<any> {
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history/aggregated`, { params: { ...params }, observe: 'response' });
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history/aggregated`, { params: { ...params }, observe: 'response' });
}
getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } });
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } });
}
getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> {
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
}
getAccelerationStats$(params: AccelerationHistoryParams): Observable<AccelerationStats> {
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`, { params: { ...params } });
return this.httpClient.get<AccelerationStats>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/stats`, { params: { ...params } });
}
setupSquare$(): Observable<{squareAppId: string, squareLocationId: string}> {
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${SERVICES_API_PREFIX}/square/setup`);
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${this.stateService.env.SERVICES_API}/square/setup`);
}
getFaucetStatus$() {
return this.httpClient.get<{ address?: string, min: number, max: number, code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'}>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' });
return this.httpClient.get<{ address?: string, min: number, max: number, code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'}>(`${this.stateService.env.SERVICES_API}/testnet4/faucet/status`, { responseType: 'json' });
}
requestTestnet4Coins$(address: string, sats: number) {
return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
return this.httpClient.get<{txid: string}>(`${this.stateService.env.SERVICES_API}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
}
generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> {
@ -177,14 +174,14 @@ export class ServicesApiServices {
product: txid,
amount: sats,
};
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params);
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin`, params);
}
retreiveInvoice$(invoiceId: string): Observable<any[]> {
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`);
return this.httpClient.get<any[]>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/invoice?id=${invoiceId}`);
}
getPaymentStatus$(orderId: string): Observable<any[]> {
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`);
return this.httpClient.get<any[]>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/check?order_id=${orderId}`);
}
}

View File

@ -75,6 +75,7 @@ export interface Env {
ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
SERVICES_API?: string;
customize?: Customization;
}
@ -109,6 +110,7 @@ const defaultEnv: Env = {
'ACCELERATOR': false,
'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false,
'SERVICES_API': 'https://mempool.space/api/v1/services',
};
@Injectable({

View File

@ -6,4 +6,4 @@
</span>
}
<ng-template #lowBalance i18n="accelerator.low-balance">Your balance is too low.<br/>Please <a style="color:#105fb0" href="/services/accelerator/overview">top up your account</a>.</ng-template>
<ng-template #lowBalance i18n="accelerator.low-balance">Your balance is too low.<br/>Please <a class="top-up-link" href="/services/accelerator/overview">top up your account</a>.</ng-template>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ You can also have the mempool.space team run a highly-performant and highly-avai
### Server Hardware
Mempool v2 is powered by [blockstream/electrs](https://github.com/Blockstream/electrs), which is a beast.
Mempool v3 is powered by [mempool/electrs](https://github.com/mempool/electrs), which is a beast.
I recommend a beefy server:

View File

@ -47,9 +47,6 @@ class Server {
case "liquid":
canonical = "https://liquid.network"
break;
case "bisq":
canonical = "https://bisq.markets"
break;
case "onbtc":
canonical = "https://bitcoin.gob.sv"
break;