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', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .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/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) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .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) { private async getBlocks(req: Request, res: Response) {
try { try {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin 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 bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import logger from '../logger'; import logger from '../logger';
import memPool from './mempool'; 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 { Common } from './common';
import diskCache from './disk-cache'; import diskCache from './disk-cache';
import transactionUtils from './transaction-utils'; 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 { public getLastDifficultyAdjustmentTime(): number {
return this.lastDifficultyAdjustmentTime; return this.lastDifficultyAdjustmentTime;
} }

View File

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

View File

@ -42,6 +42,19 @@ export interface BlockAudit {
matchRate: number, matchRate: number,
expectedFees?: number, expectedFees?: number,
expectedWeight?: 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 { export interface AuditScore {

View File

@ -1,7 +1,7 @@
import blocks from '../api/blocks'; import blocks from '../api/blocks';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import { BlockAudit, AuditScore } from '../mempool.interfaces'; import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
class BlocksAuditRepositories { class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> { 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> { public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
try { try {
const [rows]: any[] = await DB.query( 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} __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0} __SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
__ACCELERATOR__=${ACCELERATOR:=false} __ACCELERATOR__=${ACCELERATOR:=false}
__SERVICES_API__=${SERVICES_API:=false}
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false} __PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
@ -69,6 +70,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__ export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__ export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
export __ACCELERATOR__ export __ACCELERATOR__
export __SERVICES_API__
export __PUBLIC_ACCELERATIONS__ export __PUBLIC_ACCELERATIONS__
export __HISTORICAL_PRICE__ export __HISTORICAL_PRICE__
export __ADDITIONAL_CURRENCIES__ export __ADDITIONAL_CURRENCIES__

View File

@ -25,5 +25,6 @@
"HISTORICAL_PRICE": true, "HISTORICAL_PRICE": true,
"ADDITIONAL_CURRENCIES": false, "ADDITIONAL_CURRENCIES": false,
"ACCELERATOR": 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) { @if (accelerateError) {
<div class="row mb-1 text-center"> <div class="row mb-1 text-center">
<div class="col-sm"> <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> </div>
<div class="row text-center mt-1"> <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 #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
<ng-template #accelerateButton> <ng-template #accelerateButton>
@if (!couldPay && !quoteError && !(estimate?.availablePaymentMethods.bitcoin || estimate?.availablePaymentMethods.balance)) { <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 disabled" style="width: 200px"> <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"> <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> </button>
} @else { @if (quoteError || cantPayReason) {
<div class="position-relative"> <div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
<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"> </div>
<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>
}
</ng-template> </ng-template>
<ng-template #accountPayButton> <ng-template #accountPayButton>

View File

@ -8,6 +8,7 @@ import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service'; import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
@ -126,7 +127,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private etaService: EtaService, private etaService: EtaService,
private audioService: AudioService, private audioService: AudioService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private authService: AuthServiceMempool private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) { ) {
this.accelerationUUID = window.crypto.randomUUID(); this.accelerationUUID = window.crypto.randomUUID();
} }
@ -198,6 +200,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) { if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
this.fetchEstimate(); this.fetchEstimate();
} }
if (this._step === 'checkout') {
this.enterpriseService.goal(8);
}
if (this._step === 'checkout' && this.canPayWithBitcoin) { if (this._step === 'checkout' && this.canPayWithBitcoin) {
this.btcpayInvoiceFailed = false; this.btcpayInvoiceFailed = false;
this.loadingBtcpayInvoice = true; this.loadingBtcpayInvoice = true;
@ -292,6 +297,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.validateChoice(); 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) { if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
this.loadingBtcpayInvoice = true; this.loadingBtcpayInvoice = true;
this.requestBTCPayInvoice(); this.requestBTCPayInvoice();
@ -546,7 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get couldPayWithCashapp() { get couldPayWithCashapp() {
if (!this.cashappEnabled || this.stateService.referrer !== 'https://cash.app/') { if (!this.cashappEnabled) {
return false; return false;
} }
return !!this.estimate?.availablePaymentMethods?.cashapp; return !!this.estimate?.availablePaymentMethods?.cashapp;
@ -569,7 +582,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
get canPayWithCashapp() { get canPayWithCashapp() {
if (!this.cashappEnabled || !this.conversions || this.stateService.referrer !== 'https://cash.app/') { if (!this.cashappEnabled || !this.conversions) {
return false; 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"> <div class="column">
<ng-container *ngFor="let bar of bars"> <ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);"> <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 { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { Transaction } from '../../interfaces/electrs.interface';
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 { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; rate: number;
style: any; style?: Record<string,string>;
class: 'tx' | 'target' | 'max'; class: 'tx' | 'target' | 'max';
label: string; label: string;
active?: boolean; active?: boolean;
rateIndex?: number; rateIndex?: number;
fee?: number; fee?: number;
height?: number;
} }
@Component({ @Component({
@ -22,7 +18,7 @@ interface GraphBar {
templateUrl: './accelerate-fee-graph.component.html', templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'], styleUrls: ['./accelerate-fee-graph.component.scss'],
}) })
export class AccelerateFeeGraphComponent implements OnInit, OnChanges { export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() estimate: AccelerationEstimate; @Input() estimate: AccelerationEstimate;
@Input() showEstimate = false; @Input() showEstimate = false;
@ -30,13 +26,37 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() maxRateIndex: number = 0; @Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
@ViewChild('feeGraph')
container: ElementRef<HTMLDivElement>;
height: number;
observer: ResizeObserver;
stopResizeLoop = false;
bars: GraphBar[] = []; bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };
constructor(
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void { ngOnInit(): void {
this.initGraph(); 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 { ngOnChanges(): void {
this.initGraph(); this.initGraph();
} }
@ -45,44 +65,61 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
if (!this.tx || !this.estimate) { if (!this.tx || !this.estimate) {
return; 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 maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize; const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate; let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { const bars: GraphBar[] = [];
return { let lastHeight = 0;
rate: option.rate, if (hasNextBlockRate) {
style: this.getStyle(option.rate, maxRate, baseHeight), lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
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) {
bars.push({ bars.push({
rate: this.estimate.targetFeeRate, rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), height: lastHeight,
class: 'target', class: 'target',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(), label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee 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({ bars.push({
rate: baseRate, rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0), style: this.getStyle(baseHeight, 0),
height: baseHeight,
class: 'tx', class: 'tx',
label: '', label: '',
fee: this.estimate.txSummary.effectiveFee, fee: this.estimate.txSummary.effectiveFee,
}); });
this.bars = bars; this.bars = bars;
this.cd.detectChanges();
} }
getStyle(rate, maxRate, base) { getStyle(height: number, base: number): Record<string,string> {
const top = (rate / maxRate);
return { return {
height: `${(top - base) * 100}%`, height: `${height}px`,
bottom: base ? `${base * 100}%` : '0', bottom: base ? `${base}px` : '0',
} }
} }
@ -96,4 +133,20 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
onPointerMove(event) { onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; 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="acceleration-timeline box">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
<div class="timeline"> <div class="timeline">
@ -11,68 +12,141 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta) { <app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
~<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>
}
</div> </div>
</div> </div>
<div class="node-spacer"></div> <div class="node-spacer"></div>
</div> </div>
<div class="nodes">
</div> <div class="node" [id]="'first-seen'">
<div class="nodes"> <div class="seen-to-acc right"></div>
<div class="node" [id]="'first-seen'"> <div class="shape-border">
<div class="seen-to-acc right" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div> <div class="shape"></div>
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !tx.acceleration"> </div>
<div class="shape"></div> <div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
</a> <div class="time">
<div class="status"><span class="badge badge-primary">Sent</span></div> <app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
<div class="time"> </div>
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
</div> </div>
</div> <div class="interval-spacer">
<div class="interval-spacer"> <div class="seen-to-acc"></div>
<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> </div>
</div> <div class="node" [id]="'accelerated'">
<div class="interval-spacer"> <div class="seen-to-acc left"></div>
<div class="acc-to-confirmed" [class.loading]="tx.acceleration && !tx.status.confirmed"></div> <div class="acc-to-confirmed right"></div>
</div> <div class="shape-border">
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed"> <div class="shape"></div>
<div class="acc-to-confirmed left" [class.loading]="tx.acceleration && !tx.status.confirmed"></div> </div>
<a class="shape-border" [class.mined-selected]="tx.status.confirmed"> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
<div class="shape"></div> <div class="time">
</a> <app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time>
<div class="status" [style]="!tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-success">Mined</span></div> </div>
<div class="time"> </div>
@if (tx.status.block_time) { <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> <app-time kind="since" [time]="tx.status.block_time"></app-time>
} @else if (eta) { </div>
<app-time kind="until" [time]="eta?.time"></app-time>
}
</div> </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> </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>
}

View File

@ -2,6 +2,9 @@
position: relative; position: relative;
width: 100%; width: 100%;
padding: 1em 0; padding: 1em 0;
&.lower-padding {
padding: 0.5em 0 1em;
}
&::after, &::before { &::after, &::before {
content: ''; content: '';
@ -52,7 +55,7 @@
.interval, .interval-spacer { .interval, .interval-spacer {
width: 8em; width: 8em;
min-width: 5em; min-width: 8em;
max-width: 8em; max-width: 8em;
height: 32px; height: 32px;
display: flex; display: flex;
@ -69,6 +72,15 @@
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
white-space: nowrap; 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); background: var(--primary);
border-radius: 5px; border-radius: 5px;
&.loading {
animation: standardPulse 1s infinite;
}
&.left { &.left {
right: 50%; right: 50%;
} }
@ -107,8 +115,20 @@
background: var(--tertiary); background: var(--tertiary);
border-radius: 5px; border-radius: 5px;
&.loading { &.go-faster {
animation: acceleratePulse 1s infinite; 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 { &.left {
@ -118,7 +138,6 @@
left: 50%; left: 50%;
} }
} }
} }
.nodes { .nodes {
@ -133,40 +152,71 @@
margin-bottom: -8px; margin-bottom: -8px;
transform: translateY(-50%); transform: translateY(-50%);
border-radius: 50%; border-radius: 50%;
padding: 2px; cursor: pointer;
padding: 4px;
background: transparent; background: transparent;
transition: background-color 300ms, padding 300ms;
.shape { .shape {
position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 50%; border-radius: 50%;
background: white; background: white;
transition: background-color 300ms, border 300ms; z-index: 1;
} }
&.sent-selected { &.waiting {
.shape { .shape {
background: var(--primary); background: var(--grey);
} }
} }
&.accelerated-selected { .connector {
.shape { position: absolute;
background: var(--tertiary); 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;
} }
} }
}
&.mined-selected { &.accelerated {
.shape { .shape-border {
background: var(--success); animation: acceleratePulse 0.4s infinite;
} }
}
&.selected {
.shape-border {
background: var(--mainnet-alt);
} }
} }
.status { .status {
margin-top: -64px; margin-top: -64px;
.badge.badge-waiting {
opacity: 0.5;
background-color: var(--grey);
color: white;
}
.badge.badge-accelerated { .badge.badge-accelerated {
background-color: var(--tertiary); background-color: var(--tertiary);
color: white; color: white;
@ -189,9 +239,17 @@
100% { background-color: var(--tertiary) } 100% { background-color: var(--tertiary) }
} }
@keyframes standardPulse { @keyframes goFasterUp {
0% { background-color: var(--primary) } 0% { background-position-y: 0; }
50% { background-color: var(--secondary) } 100% { background-position-y: -40px; }
100% { background-color: var(--primary) } }
@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 { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '../../interfaces/electrs.interface';
@ -11,23 +11,33 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() eta: ETA; @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; acceleratedAt: number;
dir: 'rtl' | 'ltr' = 'ltr'; now: number;
accelerateRatio: number;
constructor( constructor() {}
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
ngOnInit(): void { ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
} }
ngOnChanges(changes): void { 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> </form>
</div> </div>
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" <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)" [style]="{opacity: isLoading ? 0.5 : 1}"> (chartInit)="onChartInit($event)">
</div> </div>
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading"> <div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div> <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() height: number = 300;
@Input() right: number | string = 45; @Input() right: number | string = 45;
@Input() left: number | string = 75; @Input() left: number | string = 75;
@Input() period: '3d' | '1w' | '1m' = '1w'; @Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
@Input() accelerations$: Observable<Acceleration[]>; @Input() accelerations$: Observable<Acceleration[]>;
miningWindowPreference: string; miningWindowPreference: string;
@ -48,7 +48,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
isLoading = true; isLoading = true;
formatNumber = formatNumber; formatNumber = formatNumber;
timespan = ''; timespan = '';
periodSubject$: Subject<'3d' | '1w' | '1m'> = new Subject(); periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject();
chartInstance: any = undefined; chartInstance: any = undefined;
daysAvailable: number = 0; daysAvailable: number = 0;
@ -78,7 +78,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route.fragment.subscribe((fragment) => { 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 }); this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
} }
}); });

View File

@ -4,7 +4,7 @@
<h5 class="card-title" i18n="accelerator.requests">Requests</h5> <h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text"> <div class="card-text">
<div>{{ stats.totalRequested }}</div> <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> </div>
<div class="item"> <div class="item">

View File

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

View File

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

View File

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

View File

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

View File

@ -181,8 +181,8 @@
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.miner">Miner</td> <td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD"> <td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'"> <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 }} {{ block.extras.pool.name }}
</a> </a>
</td> </td>
@ -411,7 +411,7 @@
<td class="text-wrap"> <td class="text-wrap">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount> <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"> <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>
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0"> <span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}% {{ 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> <app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
</ng-container> </ng-container>
</div> </div>
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined && showPools"> <div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary" <a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
[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> {{ block.extras.pool.name}}
</a>
</div> </div>
</div> </div>
</ng-container> </ng-container>
@ -85,7 +86,7 @@
</ng-template> </ng-template>
</div> </div>
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" <div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition"
[ngStyle]="{'left': arrowLeftPx + 'px' }"></div> [ngStyle]="{'left': arrowLeftPx + 8 + 'px' }"></div>
</div> </div>
<ng-template #loadingBlocksTemplate> <ng-template #loadingBlocksTemplate>

View File

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

View File

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

View File

@ -114,7 +114,7 @@
#arrow-up { #arrow-up {
position: relative; position: relative;
right: calc(var(--block-size) * 0.6); right: calc(var(--block-size) * 0.6);
top: calc(var(--block-size) * 1.12); top: calc(var(--block-size) * 1.20);
width: 0; width: 0;
height: 0; height: 0;
border-left: calc(var(--block-size) * 0.3) solid transparent; 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"> <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> <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> <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> </li>
</ul> </ul>
</div> </div>

View File

@ -5,29 +5,33 @@
<br><br> <br><br>
<h2>Privacy Policy</h2> <h2>Privacy Policy</h2>
<h6>Updated: November 23, 2023</h6> <h6>Updated: July 10, 2024</h6>
<br><br> <br><br>
<div class="text-left"> <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> <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> <h5>By accessing this Website, you agree to the following Privacy Policy:</h5>
<br> <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> <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>
<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>
<ul> <ul>
@ -41,35 +45,49 @@
<br> <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> <br>
<h4>DONATING TO MEMPOOL.SPACE</h4> <ng-container *ngIf="officialMempoolSpace">
<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> <h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<br> <p>If you sign up for an account on mempool.space, we may collect the following:</p>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4> <ul>
<p>If you sign up for an account on mempool.space, we may collect the following:</p> <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>
<ol> <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>
<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 a subscription to Mempool Enterprise&trade; we also collect your company name which is not shared with any third-party.</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> <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 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> </ul>
<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> <br>
<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> </ng-container>
</ol> <h4>PAYMENTS AND DONATIONS</h4>
<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> <br>

View File

@ -47,6 +47,7 @@
<div class="card-title" i18n="search.mining-pools">Mining Pools</div> <div class="card-title" i18n="search.mining-pools">Mining Pools</div>
<ng-template ngFor [ngForOf]="results.pools" let-pool let-i="index"> <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"> <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> <ngb-highlight [result]="pool.name" [term]="results.searchText"></ngb-highlight>
</button> </button>
</ng-template> </ng-template>

View File

@ -26,3 +26,11 @@
.active { .active {
background-color: var(--active-bg); 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 /> <br /><br />
<h2>Terms of Service</h2> <h2>Terms of Service</h2>
<h6>Updated: August 02, 2021</h6> <h6>Updated: July 10, 2024</h6>
<br><br> <br><br>
@ -67,6 +67,38 @@
</ng-container> </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> <p>EOF</p>
</div> </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() units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
@Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second'; @Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second';
@Input() fractionDigits: number = 0; @Input() fractionDigits: number = 0;
@Input() lowercaseStart = false;
constructor( constructor(
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
@ -106,6 +107,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
return $localize`:@@date-base.immediately:Immediately`; return $localize`:@@date-base.immediately:Immediately`;
} else if (seconds < 60) { } else if (seconds < 60) {
if (this.relative || this.kind === 'since') { 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`; return $localize`:@@date-base.just-now:Just now`;
} else if (this.kind === 'until' || this.kind === 'within') { } else if (this.kind === 'until' || this.kind === 'within') {
seconds = 60; seconds = 60;

View File

@ -75,9 +75,6 @@
} @else { } @else {
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time> <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> </span>
</ng-container> </ng-container>
<ng-template #etaSkeleton> <ng-template #etaSkeleton>

View File

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

View File

@ -153,15 +153,6 @@
<br> <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"> <ng-container *ngIf="rbfInfo">
<div class="title float-left"> <div class="title float-left">
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2> <h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
@ -171,6 +162,15 @@
<br> <br>
</ng-container> </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"> <ng-container *ngIf="flowEnabled; else flowPlaceholder">
<div class="title float-left"> <div class="title float-left">
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2> <h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
@ -449,7 +449,7 @@
<tr> <tr>
<td i18n="block.timestamp">Timestamp</td> <td i18n="block.timestamp">Timestamp</td>
<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"> <div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i> <i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
</div> </div>
@ -639,7 +639,7 @@
} }
<td> <td>
<div class="effective-fee-container"> <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> <app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
} @else { } @else {
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate> <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
@ -676,9 +676,9 @@
<td class="td-width" i18n="block.miner">Miner</td> <td class="td-width" i18n="block.miner">Miner</td>
@if (pool) { @if (pool) {
<td class="wrap-cell"> <td class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1" <a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'"> <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 }} {{ pool.name }}
</a> </a>
</td> </td>
} @else { } @else {

View File

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

View File

@ -6,7 +6,7 @@
<app-truncate [text]="tx.txid"></app-truncate> <app-truncate [text]="tx.txid"></app-truncate>
</a> </a>
<div> <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"> <ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i> <i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
</ng-template> </ng-template>

View File

@ -9017,18 +9017,89 @@ export const restApiDocsData = [
"effectiveFee": 154, "effectiveFee": 154,
"ancestorCount": 1 "ancestorCount": 1
}, },
"cost": 3850, "cost": 1386,
"targetFeeRate": 26, "targetFeeRate": 10,
"nextBlockFee": 4004, "nextBlockFee": 1540,
"userBalance": 99900000, "userBalance": 0,
"mempoolBaseFee": 40000, "mempoolBaseFee": 50000,
"vsizeFee": 50000, "vsizeFee": 0,
"hasAccess": true "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 }, options: { officialOnly: true },
type: "endpoint", type: "endpoint",
@ -9119,14 +9190,18 @@ export const restApiDocsData = [
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d", "txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
"status": "completed", "status": "completed",
"added": 1707421053, "added": 1707421053,
"lastUpdated": 1707422952, "lastUpdated": 1719134667,
"effectiveFee": 146, "effectiveFee": 146,
"effectiveVsize": 141, "effectiveVsize": 141,
"feeDelta": 14000, "feeDelta": 14000,
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003", "blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
"blockHeight": 829559, "blockHeight": 829559,
"bidBoost": 6102, "bidBoost": 3239,
"pools": [111] "boostVersion": "v1",
"pools": [
111
],
"minedByPoolUniqueId": 111
} }
]`, ]`,
}, },
@ -9229,7 +9304,7 @@ export const restApiDocsData = [
category: "accelerator-private", category: "accelerator-private",
httpRequestMethod: "POST", httpRequestMethod: "POST",
fragment: "accelerator-accelerate", fragment: "accelerator-accelerate",
title: "POST Accelerate A Transaction", title: "POST Accelerate A Transaction (Pro)",
description: { description: {
default: "<p>Sends a request to accelerate a transaction.</p>" default: "<p>Sends a request to accelerate a transaction.</p>"
}, },

View File

@ -194,7 +194,7 @@
</ng-template> </ng-template>
<ng-template type="how-to-get-transaction-confirmed-quickly"> <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>
<ng-template type="how-prevent-stuck-transaction"> <ng-template type="how-prevent-stuck-transaction">

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface';
import { Conversion } from './price.service'; import { Conversion } from './price.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
import { WebsocketResponse } from '../interfaces/websocket.interface'; import { WebsocketResponse } from '../interfaces/websocket.interface';
import { TxAuditStatus } from '../components/transaction/transaction.component';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -17,6 +18,7 @@ export class ApiService {
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>; private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
public blockAuditLoaded: { [hash: string]: boolean } = {};
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
@ -369,11 +371,18 @@ export class ApiService {
} }
getBlockAudit$(hash: string) : Observable<BlockAudit> { getBlockAudit$(hash: string) : Observable<BlockAudit> {
this.setBlockAuditLoaded(hash);
return this.httpClient.get<BlockAudit>( return this.httpClient.get<BlockAudit>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary` 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[]> { getBlockAuditScores$(from: number): Observable<AuditScore[]> {
return this.httpClient.get<AuditScore[]>( return this.httpClient.get<AuditScore[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` + 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 : '') 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() { resetBlockCache() {
this.blockHashCache = {}; this.blockHashCache = {};
this.blockCache = {}; this.blockCache = {};
this.apiService.blockAuditLoaded = {};
this.blockLoading = {}; this.blockLoading = {};
this.copiesInBlockQueue = {}; this.copiesInBlockQueue = {};
this.blockPriorities = []; this.blockPriorities = [];

View File

@ -225,4 +225,58 @@ export class EtaService {
blocks: Math.ceil(eta / da.adjustedTimeAvg), 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 { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { RelativeUrlPipe } from '../shared/pipes/relative-url/relative-url.pipe';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -30,15 +31,30 @@ export class NavigationService {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private router: Router, private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
) { ) {
this.router.events.pipe( this.router.events.pipe(
filter(event => event instanceof NavigationEnd), filter(event => event instanceof NavigationEnd),
map(() => this.router.routerState.snapshot.root), map(() => this.router.routerState.snapshot.root),
).subscribe((state) => { ).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 // For each network (bitcoin/liquid), find and save the longest url path compatible with the current route
updateSubnetPaths(root: ActivatedRouteSnapshot): void { updateSubnetPaths(root: ActivatedRouteSnapshot): void {
let path = ''; let path = '';

View File

@ -25,9 +25,6 @@ export interface IUser {
ogRank: number | null; ogRank: number | null;
} }
// Todo - move to config.json
const SERVICES_API_PREFIX = `/api/v1/services`;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -98,7 +95,7 @@ export class ServicesApiServices {
return of(null); 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[]> { getUserMenuGroups$(): Observable<MenuGroup[]> {
@ -107,7 +104,7 @@ export class ServicesApiServices {
return of(null); 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> { logout$(): Observable<any> {
@ -117,59 +114,59 @@ export class ServicesApiServices {
} }
localStorage.removeItem('auth'); localStorage.removeItem('auth');
return this.httpClient.post(`${SERVICES_API_PREFIX}/auth/logout`, {}); return this.httpClient.post(`${this.stateService.env.SERVICES_API}/auth/logout`, {});
} }
getJWT$() { 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> { 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) { 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) { 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) { 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[]> { 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> { 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[]> { 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> { 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> { 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}> { 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$() { 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) { 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> { generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> {
@ -177,14 +174,14 @@ export class ServicesApiServices {
product: txid, product: txid,
amount: sats, 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[]> { 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[]> { 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; ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string; GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string; PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
SERVICES_API?: string;
customize?: Customization; customize?: Customization;
} }
@ -109,6 +110,7 @@ const defaultEnv: Env = {
'ACCELERATOR': false, 'ACCELERATOR': false,
'PUBLIC_ACCELERATIONS': false, 'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false, 'ADDITIONAL_CURRENCIES': false,
'SERVICES_API': 'https://mempool.space/api/v1/services',
}; };
@Injectable({ @Injectable({

View File

@ -6,4 +6,4 @@
</span> </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 ### 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: I recommend a beefy server:

View File

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