Merge branch 'mempool:master' into html-quickfix
This commit is contained in:
commit
c5ef1011d8
@ -42,6 +42,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||
@ -361,6 +362,20 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getBlockTxAuditSummary(req: Request, res: Response) {
|
||||
try {
|
||||
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
|
||||
if (auditSummary) {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`transaction audit not available`);
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlocks(req: Request, res: Response) {
|
||||
try {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) { // Bitcoin
|
||||
|
||||
@ -2,7 +2,7 @@ import config from '../config';
|
||||
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||
import logger from '../logger';
|
||||
import memPool from './mempool';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit } from '../mempool.interfaces';
|
||||
import { BlockExtended, BlockExtension, BlockSummary, PoolTag, TransactionExtended, TransactionMinerInfo, CpfpSummary, MempoolTransactionExtended, TransactionClassified, BlockAudit, TransactionAudit } from '../mempool.interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
import transactionUtils from './transaction-utils';
|
||||
@ -1359,6 +1359,14 @@ class Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockTxAuditSummary(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
|
||||
return BlocksAuditsRepository.$getBlockTxAudit(hash, txid);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastDifficultyAdjustmentTime(): number {
|
||||
return this.lastDifficultyAdjustmentTime;
|
||||
}
|
||||
|
||||
@ -333,7 +333,9 @@ class Server {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
if (!config.MEMPOOL.OFFICIAL) {
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
healthCheck(): void {
|
||||
|
||||
@ -42,6 +42,19 @@ export interface BlockAudit {
|
||||
matchRate: number,
|
||||
expectedFees?: number,
|
||||
expectedWeight?: number,
|
||||
template?: any[];
|
||||
}
|
||||
|
||||
export interface TransactionAudit {
|
||||
seen?: boolean;
|
||||
expected?: boolean;
|
||||
added?: boolean;
|
||||
prioritized?: boolean;
|
||||
delayed?: number;
|
||||
accelerated?: boolean;
|
||||
conflict?: boolean;
|
||||
coinbase?: boolean;
|
||||
firstSeen?: number;
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import blocks from '../api/blocks';
|
||||
import DB from '../database';
|
||||
import logger from '../logger';
|
||||
import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
|
||||
|
||||
class BlocksAuditRepositories {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
@ -98,6 +98,41 @@ class BlocksAuditRepositories {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
try {
|
||||
const blockAudit = await this.$getBlockAudit(hash);
|
||||
|
||||
if (blockAudit) {
|
||||
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = blockAudit.acceleratedTxs.includes(txid);
|
||||
const isConflict = blockAudit.fullrbfTxs.includes(txid);
|
||||
let isExpected = false;
|
||||
let firstSeen = undefined;
|
||||
blockAudit.template?.forEach(tx => {
|
||||
if (tx.txid === txid) {
|
||||
isExpected = true;
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
logger.err(`Cannot fetch block transaction audit from db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async $getBlockAuditScore(hash: string): Promise<AuditScore> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
|
||||
3
contributors/hans-crypto.txt
Normal file
3
contributors/hans-crypto.txt
Normal 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
3
contributors/svrgnty.txt
Normal 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
|
||||
@ -40,6 +40,7 @@ __MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__SERVICES_API__=${SERVICES_API:=false}
|
||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
@ -69,6 +70,7 @@ export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||
export __ACCELERATOR__
|
||||
export __SERVICES_API__
|
||||
export __PUBLIC_ACCELERATIONS__
|
||||
export __HISTORICAL_PRICE__
|
||||
export __ADDITIONAL_CURRENCIES__
|
||||
|
||||
@ -25,5 +25,6 @@
|
||||
"HISTORICAL_PRICE": true,
|
||||
"ADDITIONAL_CURRENCIES": false,
|
||||
"ACCELERATOR": false,
|
||||
"PUBLIC_ACCELERATIONS": false
|
||||
"PUBLIC_ACCELERATIONS": false,
|
||||
"SERVICES_API": "https://mempool.space/api/v1/services"
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@if (accelerateError) {
|
||||
<div class="row mb-1 text-center">
|
||||
<div class="col-sm">
|
||||
<h1 style="font-size: larger;">Sorry, something went wrong!</h1>
|
||||
<h1 style="font-size: larger;" i18n="accelerator.sorry-error-title">Sorry, something went wrong!</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center mt-1">
|
||||
@ -552,22 +552,15 @@
|
||||
<ng-template #accelerateTo let-x i18n="accelerator.accelerate-to-x">Accelerate to ~{{ x | number : '1.0-0' }} sat/vB</ng-template>
|
||||
|
||||
<ng-template #accelerateButton>
|
||||
@if (!couldPay && !quoteError && !(estimate?.availablePaymentMethods.bitcoin || estimate?.availablePaymentMethods.balance)) {
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center disabled" style="width: 200px">
|
||||
<div class="position-relative">
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
|
||||
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
||||
<span>Coming soon</span>
|
||||
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
|
||||
</button>
|
||||
} @else {
|
||||
<div class="position-relative">
|
||||
<button type="button" class="mt-1 btn btn-purple rounded-pill align-self-center d-flex flex-row justify-content-center align-items-center" [class.disabled]="!canPay || quoteError || cantPayReason || calculating || (!advancedEnabled && selectedOption !== 'accel')" style="width: 200px" (click)="moveToStep('checkout')">
|
||||
<img src="/resources/mempool-accelerator-sparkles-light.svg" height="20" class="mr-2" style="margin-left: -10px">
|
||||
<span i18n="transaction.accelerate|Accelerate button label">Accelerate</span>
|
||||
</button>
|
||||
@if (quoteError || cantPayReason) {
|
||||
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (quoteError || cantPayReason) {
|
||||
<div class="btn-error-wrapper"><span class="btn-error"><app-mempool-error [error]="quoteError || cantPayReason" [textOnly]="true" alertClass=""></app-mempool-error></span></div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #accountPayButton>
|
||||
|
||||
@ -8,6 +8,7 @@ import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
|
||||
|
||||
@ -126,7 +127,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private etaService: EtaService,
|
||||
private audioService: AudioService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private authService: AuthServiceMempool
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.accelerationUUID = window.crypto.randomUUID();
|
||||
}
|
||||
@ -198,6 +200,9 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (!this.estimate && ['quote', 'summary', 'checkout'].includes(this.step)) {
|
||||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.enterpriseService.goal(8);
|
||||
}
|
||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||
this.btcpayInvoiceFailed = false;
|
||||
this.loadingBtcpayInvoice = true;
|
||||
@ -292,6 +297,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
|
||||
this.validateChoice();
|
||||
|
||||
if (!this.couldPay) {
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
if (this.step === 'summary') {
|
||||
this.unavailable.emit(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.step === 'checkout' && this.canPayWithBitcoin && !this.loadingBtcpayInvoice) {
|
||||
this.loadingBtcpayInvoice = true;
|
||||
this.requestBTCPayInvoice();
|
||||
@ -546,7 +559,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get couldPayWithCashapp() {
|
||||
if (!this.cashappEnabled || this.stateService.referrer !== 'https://cash.app/') {
|
||||
if (!this.cashappEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||
@ -569,7 +582,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get canPayWithCashapp() {
|
||||
if (!this.cashappEnabled || !this.conversions || this.stateService.referrer !== 'https://cash.app/') {
|
||||
if (!this.cashappEnabled || !this.conversions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="fee-graph" *ngIf="tx && estimate">
|
||||
<div class="fee-graph" *ngIf="tx && estimate" #feeGraph>
|
||||
<div class="column">
|
||||
<ng-container *ngFor="let bar of bars">
|
||||
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
style: any;
|
||||
style?: Record<string,string>;
|
||||
class: 'tx' | 'target' | 'max';
|
||||
label: string;
|
||||
active?: boolean;
|
||||
rateIndex?: number;
|
||||
fee?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -22,7 +18,7 @@ interface GraphBar {
|
||||
templateUrl: './accelerate-fee-graph.component.html',
|
||||
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||
})
|
||||
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
|
||||
@Input() tx: Transaction;
|
||||
@Input() estimate: AccelerationEstimate;
|
||||
@Input() showEstimate = false;
|
||||
@ -30,13 +26,37 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
@Input() maxRateIndex: number = 0;
|
||||
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||
|
||||
@ViewChild('feeGraph')
|
||||
container: ElementRef<HTMLDivElement>;
|
||||
height: number;
|
||||
observer: ResizeObserver;
|
||||
stopResizeLoop = false;
|
||||
|
||||
bars: GraphBar[] = [];
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (ResizeObserver) {
|
||||
this.observer = new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
this.height = entry.contentRect.height;
|
||||
this.initGraph();
|
||||
}
|
||||
});
|
||||
this.observer.observe(this.container.nativeElement);
|
||||
} else {
|
||||
this.startResizeFallbackLoop();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
@ -45,44 +65,61 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
if (!this.tx || !this.estimate) {
|
||||
return;
|
||||
}
|
||||
const hasNextBlockRate = (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee);
|
||||
const numBars = hasNextBlockRate ? 4 : 3;
|
||||
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||
const baseHeight = baseRate / maxRate;
|
||||
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||
return {
|
||||
rate: option.rate,
|
||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||
class: 'max',
|
||||
label: this.showEstimate ? $localize`maximum` : $localize`:@@25fbf6e80a945703c906a5a7d8c92e8729c7ab21:accelerated`,
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
|
||||
let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
|
||||
const bars: GraphBar[] = [];
|
||||
let lastHeight = 0;
|
||||
if (hasNextBlockRate) {
|
||||
lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
height: lastHeight,
|
||||
class: 'target',
|
||||
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
}
|
||||
this.maxRateOptions.forEach((option, index) => {
|
||||
lastHeight = Math.max(lastHeight + 30, (this.height * ((option.rate - baseRate) / maxRate)));
|
||||
bars.push({
|
||||
rate: option.rate,
|
||||
height: lastHeight,
|
||||
class: 'max',
|
||||
label: this.showEstimate ? $localize`maximum` : $localize`accelerated`,
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
})
|
||||
})
|
||||
|
||||
bars.reverse();
|
||||
|
||||
baseHeight = this.height - lastHeight;
|
||||
|
||||
for (const bar of bars) {
|
||||
bar.style = this.getStyle(bar.height, baseHeight);
|
||||
}
|
||||
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
style: this.getStyle(baseHeight, 0),
|
||||
height: baseHeight,
|
||||
class: 'tx',
|
||||
label: '',
|
||||
fee: this.estimate.txSummary.effectiveFee,
|
||||
});
|
||||
|
||||
this.bars = bars;
|
||||
this.cd.detectChanges();
|
||||
}
|
||||
|
||||
getStyle(rate, maxRate, base) {
|
||||
const top = (rate / maxRate);
|
||||
getStyle(height: number, base: number): Record<string,string> {
|
||||
return {
|
||||
height: `${(top - base) * 100}%`,
|
||||
bottom: base ? `${base * 100}%` : '0',
|
||||
height: `${height}px`,
|
||||
bottom: base ? `${base}px` : '0',
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,4 +133,20 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
|
||||
startResizeFallbackLoop(): void {
|
||||
if (this.stopResizeLoop) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.height = this.container?.nativeElement?.clientHeight || 0;
|
||||
this.initGraph();
|
||||
this.startResizeFallbackLoop();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopResizeLoop = true;
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="acceleration-timeline box">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
@ -11,68 +12,141 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time kind="plain" [time]="eta?.wait / 1000"></app-time>
|
||||
} @else if (tx.status.block_time) {
|
||||
<app-time kind="plain" [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
}
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !tx.acceleration">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status"><span class="badge badge-primary">Sent</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
|
||||
<div class="acc-to-confirmed right" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.accelerated-selected]="tx.acceleration && !tx.status.confirmed">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status" [style]="!tx.acceleration && !tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed">
|
||||
<div class="acc-to-confirmed left" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
|
||||
<a class="shape-border" [class.mined-selected]="tx.status.confirmed">
|
||||
<div class="shape"></div>
|
||||
</a>
|
||||
<div class="status" [style]="!tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-success">Mined</span></div>
|
||||
<div class="time">
|
||||
@if (tx.status.block_time) {
|
||||
<div class="node" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt" [lowercaseStart]="true"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="acc-to-confirmed"></div>
|
||||
</div>
|
||||
<div class="node selected" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left" ></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
<div class="time">
|
||||
<app-time kind="since" [time]="tx.status.block_time"></app-time>
|
||||
} @else if (eta) {
|
||||
<app-time kind="until" [time]="eta?.time"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (acceleratedETA) { <!-- Not yet accelerated; to be shown only in acceleration checkout -->
|
||||
} @else if (standardETA) { <!-- Accelerated -->
|
||||
<div class="acceleration-timeline box lower-padding">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<div class="node">
|
||||
<div class="acc-to-confirmed right go-faster"></div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="acc-to-confirmed left go-faster"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape animate"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
-
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
</div>
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||
<div class="time">
|
||||
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node accelerated" [id]="'accelerated'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="seen-to-acc right"></div>
|
||||
<div class="shape-border">
|
||||
<div class="shape"></div>
|
||||
<div class="connector down loading"></div>
|
||||
</div>
|
||||
<div class="time" style="margin-top: 3px;">
|
||||
<span i18n="transaction.audit.accelerated">Accelerated</span> <app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="interval-spacer">
|
||||
<div class="seen-to-acc"></div>
|
||||
</div>
|
||||
<div class="node" [id]="'confirmed'">
|
||||
<div class="seen-to-acc left"></div>
|
||||
<div class="shape-border waiting">
|
||||
<div class="shape"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #nodeSpacer>
|
||||
<div class="node-spacer"></div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #intervalSpacer>
|
||||
<div class="interval-spacer"></div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
}
|
||||
@ -2,6 +2,9 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 1em 0;
|
||||
&.lower-padding {
|
||||
padding: 0.5em 0 1em;
|
||||
}
|
||||
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
@ -52,7 +55,7 @@
|
||||
|
||||
.interval, .interval-spacer {
|
||||
width: 8em;
|
||||
min-width: 5em;
|
||||
min-width: 8em;
|
||||
max-width: 8em;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
@ -69,6 +72,15 @@
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
.compare {
|
||||
font-style: italic;
|
||||
color: var(--mainnet-alt);
|
||||
font-weight: 600;
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,10 +96,6 @@
|
||||
background: var(--primary);
|
||||
border-radius: 5px;
|
||||
|
||||
&.loading {
|
||||
animation: standardPulse 1s infinite;
|
||||
}
|
||||
|
||||
&.left {
|
||||
right: 50%;
|
||||
}
|
||||
@ -107,8 +115,20 @@
|
||||
background: var(--tertiary);
|
||||
border-radius: 5px;
|
||||
|
||||
&.loading {
|
||||
animation: acceleratePulse 1s infinite;
|
||||
&.go-faster {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath style='fill:%239339f4;' d='M 0,0 5,5 0,10 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,0 10,0 15,5 10,10 0,10 5,5 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 10,0 20,0 20,10 10,10 15,5 Z'/%3E%3C/svg%3E%0A"); background-size: 20px 10px;
|
||||
border-radius: 0;
|
||||
|
||||
&.right {
|
||||
left: calc(50% + 5px);
|
||||
margin-right: calc(-4em + 5px);
|
||||
animation: goFasterRight 0.8s infinite linear;
|
||||
}
|
||||
&.left {
|
||||
right: calc(50% + 5px);
|
||||
margin-left: calc(-4em + 5px);
|
||||
animation: goFasterLeft 0.8s infinite linear;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
@ -118,7 +138,6 @@
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.nodes {
|
||||
@ -133,39 +152,70 @@
|
||||
margin-bottom: -8px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
transition: background-color 300ms, padding 300ms;
|
||||
|
||||
|
||||
.shape {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
transition: background-color 300ms, border 300ms;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.sent-selected {
|
||||
&.waiting {
|
||||
.shape {
|
||||
background: var(--primary);
|
||||
background: var(--grey);
|
||||
}
|
||||
}
|
||||
|
||||
&.accelerated-selected {
|
||||
.shape {
|
||||
background: var(--tertiary);
|
||||
|
||||
.connector {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
height: 88px;
|
||||
width: 10px;
|
||||
left: -5px;
|
||||
top: -73px;
|
||||
transform: translateX(120%);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='20'%3E%3Cpath style='fill:%239339f4;' d='M 0,20 5,15 10,20 Z'/%3E%3Cpath style='fill:%23653b9c;' d='M 0,20 5,15 10,20 10,10 5,5 0,10 Z'/%3E%3Cpath style='fill:%239339f4;' d='M 0,10 5,5 10,10 10,0 0,0 Z'/%3E%3C/svg%3E%0A"); // linear-gradient(135deg, var(--tertiary) 34%, transparent 34%),
|
||||
background-size: 10px 20px;
|
||||
|
||||
&.down {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
&.up {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
animation: goFasterUp 0.8s infinite linear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.accelerated {
|
||||
.shape-border {
|
||||
animation: acceleratePulse 0.4s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.mined-selected {
|
||||
.shape {
|
||||
background: var(--success);
|
||||
}
|
||||
&.selected {
|
||||
.shape-border {
|
||||
background: var(--mainnet-alt);
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: -64px;
|
||||
|
||||
.badge.badge-waiting {
|
||||
opacity: 0.5;
|
||||
background-color: var(--grey);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge.badge-accelerated {
|
||||
background-color: var(--tertiary);
|
||||
@ -189,9 +239,17 @@
|
||||
100% { background-color: var(--tertiary) }
|
||||
}
|
||||
|
||||
@keyframes standardPulse {
|
||||
0% { background-color: var(--primary) }
|
||||
50% { background-color: var(--secondary) }
|
||||
100% { background-color: var(--primary) }
|
||||
|
||||
}
|
||||
@keyframes goFasterUp {
|
||||
0% { background-position-y: 0; }
|
||||
100% { background-position-y: -40px; }
|
||||
}
|
||||
|
||||
@keyframes goFasterLeft {
|
||||
0% { background-position: left 0px bottom 0px }
|
||||
100% { background-position: left 40px bottom 0px; }
|
||||
}
|
||||
|
||||
@keyframes goFasterRight {
|
||||
0% { background-position: right 0 bottom 0px; }
|
||||
100% { background-position: right -40px bottom 0px; }
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnChanges } from '@angular/core';
|
||||
import { ETA } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
|
||||
@ -11,23 +11,33 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() transactionTime: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() eta: ETA;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@Input() standardETA: number;
|
||||
@Input() acceleratedETA: number;
|
||||
|
||||
acceleratedAt: number;
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
now: number;
|
||||
accelerateRatio: number;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.dir = 'rtl';
|
||||
}
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
|
||||
|
||||
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
|
||||
// if (changes?.eta?.currentValue) {
|
||||
// if (changes?.acceleratedETA?.currentValue) {
|
||||
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
|
||||
// } else if (changes?.standardETA?.currentValue) {
|
||||
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -45,8 +45,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
||||
@ -32,7 +32,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
@Input() height: number = 300;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 75;
|
||||
@Input() period: '3d' | '1w' | '1m' = '1w';
|
||||
@Input() period: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
@Input() accelerations$: Observable<Acceleration[]>;
|
||||
|
||||
miningWindowPreference: string;
|
||||
@ -48,7 +48,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
periodSubject$: Subject<'3d' | '1w' | '1m'> = new Subject();
|
||||
periodSubject$: Subject<'24h' | '3d' | '1w' | '1m' | 'all'> = new Subject();
|
||||
chartInstance: any = undefined;
|
||||
daysAvailable: number = 0;
|
||||
|
||||
@ -78,7 +78,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.route.fragment.subscribe((fragment) => {
|
||||
if (['24h', '3d', '1w', '1m', '3m'].indexOf(fragment) > -1) {
|
||||
if (['24h', '3d', '1w', '1m', '3m', 'all'].indexOf(fragment) > -1) {
|
||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||
}
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<h5 class="card-title" i18n="accelerator.requests">Requests</h5>
|
||||
<div class="card-text">
|
||||
<div>{{ stats.totalRequested }}</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
|
||||
<div class="symbol" i18n="accelerator.total-accelerated-plural">accelerated</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
|
||||
@ -16,7 +16,7 @@ export type AccelerationStats = {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
@Input() timespan: '3d' | '1w' | '1m' = '1w';
|
||||
@Input() timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
accelerationStats$: Observable<AccelerationStats>;
|
||||
blocksInPeriod: number = 7 * 144;
|
||||
|
||||
@ -35,6 +35,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
updateStats(): void {
|
||||
this.accelerationStats$ = this.servicesApiService.getAccelerationStats$({ timeframe: this.timespan });
|
||||
switch (this.timespan) {
|
||||
case '24h':
|
||||
this.blocksInPeriod = 144;
|
||||
break;
|
||||
case '3d':
|
||||
this.blocksInPeriod = 3 * 144;
|
||||
break;
|
||||
@ -44,6 +47,9 @@ export class AccelerationStatsComponent implements OnInit, OnChanges {
|
||||
case '1m':
|
||||
this.blocksInPeriod = 30 * 144;
|
||||
break;
|
||||
case 'all':
|
||||
this.blocksInPeriod = Infinity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,12 +23,18 @@
|
||||
<div class="main-title">
|
||||
<span [attr.data-cy]="'acceleration-stats'" i18n="accelerator.acceleration-stats">Acceleration stats</span>
|
||||
@switch (timespan) {
|
||||
@case ('24h') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-day">(1 day)</span>
|
||||
}
|
||||
@case ('1w') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-week">(1 week)</span>
|
||||
}
|
||||
@case ('1m') {
|
||||
<span style="font-size: xx-small" i18n="mining.1-month">(1 month)</span>
|
||||
}
|
||||
@case ('all') {
|
||||
<span style="font-size: xx-small" i18n="mining.all-time">(all time)</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
@ -36,11 +42,17 @@
|
||||
<div class="card-body more-padding">
|
||||
<app-acceleration-stats [timespan]="timespan"></app-acceleration-stats>
|
||||
<div class="widget-toggler">
|
||||
<a href="" (click)="setTimespan('24h')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '24h'}"><small>24h</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('1w')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '1w'}"><small>1w</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('1m')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === '1m'}"><small>1m</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="" (click)="setTimespan('all')" class="toggler-option"
|
||||
[ngClass]="{'inactive': timespan === 'all'}"><small>all</small></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -37,7 +37,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
webGlEnabled = true;
|
||||
seen: Set<string> = new Set();
|
||||
firstLoad = true;
|
||||
timespan: '3d' | '1w' | '1m' = '1w';
|
||||
timespan: '24h' | '3d' | '1w' | '1m' | 'all' = '1w';
|
||||
|
||||
accelerationDeltaSubscription: Subscription;
|
||||
|
||||
@ -99,7 +99,7 @@ export class AcceleratorDashboardComponent implements OnInit, OnDestroy {
|
||||
this.minedAccelerations$ = this.stateService.chainTip$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed', pageLength: 6 }).pipe(
|
||||
return this.serviceApiServices.getAccelerationHistory$({ status: 'completed_provisional,completed', pageLength: 6 }).pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}),
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
|
||||
.menu-toggle {
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
height: 1.8em;
|
||||
padding: 0px 1px;
|
||||
opacity: 0;
|
||||
@ -42,6 +43,7 @@
|
||||
border: none;
|
||||
border-radius: 0.35em;
|
||||
pointer-events: all;
|
||||
align-self: normal;
|
||||
}
|
||||
|
||||
.filter-menu {
|
||||
|
||||
@ -181,8 +181,8 @@
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||
[class]="block.extras.pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
@ -411,7 +411,7 @@
|
||||
<td class="text-wrap">
|
||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
<span *ngIf="oobFees" class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band">
|
||||
<app-amount [satoshis]="oobFees" digitsInfo="1.2-8" [noFiat]="true" [addPlus]="true"></app-amount>
|
||||
<app-amount [satoshis]="oobFees" digitsInfo="1.8-8" [noFiat]="true" [addPlus]="true"></app-amount>
|
||||
</span>
|
||||
<span *ngIf="blockAudit.feeDelta" class="difference" [class.positive]="blockAudit.feeDelta <= 0" [class.negative]="blockAudit.feeDelta > 0">
|
||||
{{ blockAudit.feeDelta < 0 ? '+' : '' }}{{ (-blockAudit.feeDelta * 100) | amountShortener: 2 }}%
|
||||
|
||||
@ -272,3 +272,11 @@ h1 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
@ -59,10 +59,11 @@
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [precision]="1" minUnit="minute"></app-time></div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="animated" [class]="markHeight === block.height ? 'hide' : 'show'" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||
[routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
{{ block.extras.pool.name}}</a>
|
||||
<div class="animated" *ngIf="block.extras?.pool != undefined && showPools">
|
||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
{{ block.extras.pool.name}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@ -85,7 +86,7 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition"
|
||||
[ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||
[ngStyle]="{'left': arrowLeftPx + 8 + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingBlocksTemplate>
|
||||
|
||||
@ -125,12 +125,12 @@
|
||||
#arrow-up {
|
||||
position: relative;
|
||||
left: calc(var(--block-size) * 0.6);
|
||||
top: calc(var(--block-size) * 1.12);
|
||||
top: calc(var(--block-size) * 1.28);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: calc(var(--block-size) * 0.3) solid transparent;
|
||||
border-right: calc(var(--block-size) * 0.3) solid transparent;
|
||||
border-bottom: calc(var(--block-size) * 0.3) solid var(--fg);
|
||||
border-left: calc(var(--block-size) * 0.2) solid transparent;
|
||||
border-right: calc(var(--block-size) * 0.2) solid transparent;
|
||||
border-bottom: calc(var(--block-size) * 0.2) solid var(--fg);
|
||||
}
|
||||
|
||||
.flashing {
|
||||
@ -157,17 +157,20 @@
|
||||
position: relative;
|
||||
top: 15px;
|
||||
z-index: 101;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.animated {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
.show {
|
||||
opacity: 1;
|
||||
}
|
||||
.hide {
|
||||
opacity: 0.4;
|
||||
pointer-events : none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-ltr {
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
}
|
||||
|
||||
.blockchain-wrapper {
|
||||
height: 250px;
|
||||
|
||||
height: 260px;
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* IE10+/Edge */
|
||||
@ -57,7 +56,7 @@
|
||||
color: var(--fg);
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: -1.8em;
|
||||
bottom: -2.2em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%);
|
||||
background: none;
|
||||
|
||||
@ -114,7 +114,7 @@
|
||||
#arrow-up {
|
||||
position: relative;
|
||||
right: calc(var(--block-size) * 0.6);
|
||||
top: calc(var(--block-size) * 1.12);
|
||||
top: calc(var(--block-size) * 1.20);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: calc(var(--block-size) * 0.3) solid transparent;
|
||||
|
||||
@ -34,7 +34,12 @@
|
||||
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
|
||||
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
|
||||
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
|
||||
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
|
||||
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">
|
||||
{{ item.title }}
|
||||
@if (item.isExternal === true) {
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="margin-left: 5px; font-size: 13px; color: lightgray"></fa-icon>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -5,29 +5,33 @@
|
||||
<br><br>
|
||||
|
||||
<h2>Privacy Policy</h2>
|
||||
<h6>Updated: November 23, 2023</h6>
|
||||
<h6>Updated: July 10, 2024</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
<div class="text-left">
|
||||
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
<h4>USE YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project®; on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website. By using your own self-hosted instance you will have maximum security, privacy and freedom.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<h5>By accessing this Website, you agree to the following Privacy Policy:</h5>
|
||||
|
||||
<br>
|
||||
|
||||
<h4>TRUSTED THIRD PARTIES ARE SECURITY HOLES</h4>
|
||||
<h4>General</h4>
|
||||
|
||||
<p>Out of respect for the Bitcoin community, this website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this website.</p>
|
||||
<p *ngIf="officialMempoolSpace">Out of respect for the Bitcoin community, this Website does not use any third-party analytics, third-party trackers, or third-party cookies, and we do not share any private user data with third-parties. Additionally, to mitigate the risk of surveillance by malicious third-parties, we self-host this Website on our own hardware and network infrastructure, so there are no "hosting companies" or "cloud providers" involved with the operation of this Website.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<h4>TRUSTED FIRST PARTIES ARE ALSO SECURITY HOLES</h4>
|
||||
|
||||
<p>Out of respect for the Bitcoin community, this website does not use any first-party cookies, except to store your preferred language setting (if any). However, we do use minimal first-party analytics and logging as needed for the operation of this website, as follows:</p>
|
||||
<p>Out of respect for the Bitcoin community, this Website does not use any first-party cookies, except to store your preferred language setting (if any). However, we do use minimal first-party analytics and logging as needed for the operation of this Website, as follows:</p>
|
||||
|
||||
<ul>
|
||||
|
||||
@ -41,35 +45,49 @@
|
||||
|
||||
<br>
|
||||
|
||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
<h4>USING MEMPOOL ACCELERATOR™</h4>
|
||||
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® 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™ your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
||||
|
||||
<p *ngIf="!officialMempoolSpace">If you click the accelerate button on a transaction you will load acceleration pricing information from Mempool. If you make an acceleration request, the TXID and your maximum bid will be sent to Mempool who will store and share this information with their mining pool partners, and publicly display accelerated transaction details on mempool.space and via Mempool's APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="officialMempoolSpace">
|
||||
|
||||
<h4>DONATING TO MEMPOOL.SPACE</h4>
|
||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||
|
||||
<p>If you donate to mempool.space, your payment information and your Twitter identity (if provided) will be collected in a database, which may be used to publicly display the sponsor profiles on <a href="https://mempool.space/about">mempool.space/about</a>. Thank you for supporting The Mempool Open Source Project.</p>
|
||||
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
|
||||
|
||||
<br>
|
||||
<ul>
|
||||
|
||||
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
|
||||
<li>Your e-mail address and/or country; we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as necessary for our fiat payment processor (see "Payments" below).</li>
|
||||
|
||||
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
|
||||
<li>If you connect your X (fka Twitter) account, we may store your X identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project®, claim your Lightning node, or other such use cases.</li>
|
||||
|
||||
<ol>
|
||||
<li>If you sign up for a subscription to Mempool Enterprise™ we also collect your company name which is not shared with any third-party.</li>
|
||||
|
||||
<li>If you provide your name, country, and/or e-mail address, we may use this information to manage your user account, for billing purposes, or to update you about our services. We will not share this with any third-party, except as detailed below if you sponsor The Mempool Open Source Project®, purchase a subscription to Mempool Enterprise®, or accelerate transactions using Mempool Accelerator™.</li>
|
||||
<li>If you sign up for an account on mempool.space and use Mempool Accelerator™ Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
|
||||
|
||||
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
|
||||
</ul>
|
||||
|
||||
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
|
||||
<br>
|
||||
|
||||
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
|
||||
</ng-container>
|
||||
|
||||
<li>If you accelerate transactions using Mempool Accelerator™, we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
|
||||
<h4>PAYMENTS AND DONATIONS</h4>
|
||||
|
||||
</ol>
|
||||
<p>If you make any payment to Mempool or donation to The Mempool Open Source Project®, 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>
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
<div class="card-title" i18n="search.mining-pools">Mining Pools</div>
|
||||
<ng-template ngFor [ngForOf]="results.pools" let-pool let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [class.inactive]="!pool.active" type="button" role="option" class="dropdown-item">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
<ngb-highlight [result]="pool.name" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@ -26,3 +26,11 @@
|
||||
.active {
|
||||
background-color: var(--active-bg);
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<br /><br />
|
||||
|
||||
<h2>Terms of Service</h2>
|
||||
<h6>Updated: August 02, 2021</h6>
|
||||
<h6>Updated: July 10, 2024</h6>
|
||||
|
||||
<br><br>
|
||||
|
||||
@ -67,6 +67,38 @@
|
||||
|
||||
</ng-container>
|
||||
|
||||
<h4>MEMPOOL ACCELERATOR™</h4>
|
||||
|
||||
<p><a href="https://mempool.space/accelerator">Mempool Accelerator™</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™ account credit top-ups are non-refundable. </li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Mempool Accelerator™ account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
|
||||
|
||||
<br>
|
||||
|
||||
<li>Mempool does not provide acceleration services to persons in Cuba, Iran, North Korea, Russia, Syria, Crimea, Donetsk or Luhansk Regions of Ukraine.</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
|
||||
<p>EOF</p>
|
||||
|
||||
</div>
|
||||
|
||||
@ -35,6 +35,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
@Input() minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second';
|
||||
@Input() fractionDigits: number = 0;
|
||||
@Input() lowercaseStart = false;
|
||||
|
||||
constructor(
|
||||
private ref: ChangeDetectorRef,
|
||||
@ -106,6 +107,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return $localize`:@@date-base.immediately:Immediately`;
|
||||
} else if (seconds < 60) {
|
||||
if (this.relative || this.kind === 'since') {
|
||||
if (this.lowercaseStart) {
|
||||
return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1);
|
||||
}
|
||||
return $localize`:@@date-base.just-now:Just now`;
|
||||
} else if (this.kind === 'until' || this.kind === 'within') {
|
||||
seconds = 60;
|
||||
|
||||
@ -75,9 +75,6 @@
|
||||
} @else {
|
||||
<app-time kind="until" [time]="eta.time" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
}
|
||||
<!-- @if (!showAccelerationSummary && isMobile && !tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration) {
|
||||
<a class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
} -->
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-template #etaSkeleton>
|
||||
|
||||
@ -728,7 +728,6 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
if (!this.txId) {
|
||||
return;
|
||||
}
|
||||
this.enterpriseService.goal(8);
|
||||
this.accelerationFlowCompleted = false;
|
||||
if (this.showAccelerationSummary) {
|
||||
this.scrollIntoAccelPreview = true;
|
||||
|
||||
@ -153,15 +153,6 @@
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactionTime && (tx.acceleration || isAcceleration)">
|
||||
<div class="title float-left">
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="rbfInfo">
|
||||
<div class="title float-left">
|
||||
<h2 id="rbf" i18n="transaction.rbf-history|RBF Timeline">RBF Timeline</h2>
|
||||
@ -171,6 +162,15 @@
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="transactionTime && isAcceleration">
|
||||
<div class="title float-left">
|
||||
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline>
|
||||
<br>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||
<div class="title float-left">
|
||||
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
@ -449,7 +449,7 @@
|
||||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
<div class="lg-inline">
|
||||
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
|
||||
</div>
|
||||
@ -639,7 +639,7 @@
|
||||
}
|
||||
<td>
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
@ -676,9 +676,9 @@
|
||||
<td class="td-width" i18n="block.miner">Miner</td>
|
||||
@if (pool) {
|
||||
<td class="wrap-cell">
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge mr-1"
|
||||
[class]="pool.slug === 'unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||
{{ pool.name }}
|
||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||
{{ pool.name }}
|
||||
</a>
|
||||
</td>
|
||||
} @else {
|
||||
|
||||
@ -324,4 +324,12 @@
|
||||
.goggles-icon {
|
||||
display: block;
|
||||
width: 2.7em;
|
||||
}
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ interface Pool {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface AuditStatus {
|
||||
export interface TxAuditStatus {
|
||||
seen?: boolean;
|
||||
expected?: boolean;
|
||||
added?: boolean;
|
||||
@ -65,6 +65,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txId: string;
|
||||
txInBlockIndex: number;
|
||||
mempoolPosition: MempoolPosition;
|
||||
gotInitialPosition = false;
|
||||
accelerationPositions: AccelerationPosition[];
|
||||
isLoadingTx = true;
|
||||
error: any = undefined;
|
||||
@ -99,7 +100,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
sigops: number | null;
|
||||
adjustedVsize: number | null;
|
||||
pool: Pool | null;
|
||||
auditStatus: AuditStatus | null;
|
||||
auditStatus: TxAuditStatus | null;
|
||||
isAcceleration: boolean = false;
|
||||
filters: Filter[] = [];
|
||||
showCpfpDetails = false;
|
||||
@ -112,6 +113,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
|
||||
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
|
||||
ETA$: Observable<ETA | null>;
|
||||
standardETA$: Observable<ETA | null>;
|
||||
isCached: boolean = false;
|
||||
now = Date.now();
|
||||
da$: Observable<DifficultyAdjustment>;
|
||||
@ -130,6 +132,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
tooltipPosition: { x: number, y: number };
|
||||
isMobile: boolean;
|
||||
firstLoad = true;
|
||||
waitingForAccelerationInfo: boolean = false;
|
||||
|
||||
featuresEnabled: boolean;
|
||||
segwitEnabled: boolean;
|
||||
@ -315,11 +318,19 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.setIsAccelerated();
|
||||
}),
|
||||
switchMap((blockHeight: number) => {
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight });
|
||||
return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe(
|
||||
switchMap((accelerationHistory: Acceleration[]) => {
|
||||
if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry
|
||||
return throwError('retry');
|
||||
}
|
||||
return of(accelerationHistory);
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
})
|
||||
).subscribe((accelerationHistory) => {
|
||||
for (const acceleration of accelerationHistory) {
|
||||
if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional')) {
|
||||
@ -328,13 +339,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
acceleration.boost = boostCost;
|
||||
this.tx.acceleratedAt = acceleration.added;
|
||||
this.accelerationInfo = acceleration;
|
||||
this.waitingForAccelerationInfo = false;
|
||||
this.setIsAccelerated();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.miningSubscription = this.fetchMiningInfo$.pipe(
|
||||
filter((target) => target.txid === this.txId),
|
||||
filter((target) => target.txid === this.txId && !this.pool),
|
||||
tap(() => {
|
||||
this.pool = null;
|
||||
}),
|
||||
@ -362,33 +374,41 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
) : of(isCoinbase ? { coinbase: true } : null);
|
||||
if (fetchAudit) {
|
||||
// If block audit is already cached, use it to get transaction audit
|
||||
const blockAuditLoaded = this.apiService.getBlockAuditLoaded(hash);
|
||||
if (blockAuditLoaded) {
|
||||
return this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
const firstSeen = audit.template.find(tx => tx.txid === txid)?.time;
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
};
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return this.apiService.getBlockTxAudit$(hash, txid).pipe(
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return of(isCoinbase ? { coinbase: true } : null);
|
||||
}
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(auditStatus => {
|
||||
this.auditStatus = auditStatus;
|
||||
if (this.auditStatus?.firstSeen) {
|
||||
@ -431,9 +451,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (txPosition.position?.block > 0 && this.tx.weight < 4000) {
|
||||
this.cashappEligible = true;
|
||||
}
|
||||
if (!this.gotInitialPosition && txPosition.position?.block === 0 && txPosition.position?.vsize < 750_000) {
|
||||
this.accelerationFlowCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.gotInitialPosition = true;
|
||||
} else {
|
||||
this.mempoolPosition = null;
|
||||
this.accelerationPositions = null;
|
||||
@ -602,12 +626,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.stateService.txConfirmed$.subscribe(([txConfirmed, block]) => {
|
||||
if (txConfirmed && this.tx && !this.tx.status.confirmed && txConfirmed === this.tx.txid) {
|
||||
if (this.tx.acceleration) {
|
||||
this.waitingForAccelerationInfo = true;
|
||||
}
|
||||
this.tx.status = {
|
||||
confirmed: true,
|
||||
block_height: block.height,
|
||||
block_hash: block.id,
|
||||
block_time: block.timestamp,
|
||||
};
|
||||
this.pool = block.extras.pool;
|
||||
this.txChanged$.next(true);
|
||||
this.stateService.markBlock$.next({ blockHeight: block.height });
|
||||
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
|
||||
@ -718,7 +746,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
document.location.hash = '#accelerate';
|
||||
this.enterpriseService.goal(8);
|
||||
this.openAccelerator();
|
||||
this.scrollIntoAccelPreview = true;
|
||||
return false;
|
||||
@ -797,7 +824,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
this.isAcceleration = ((this.tx.acceleration && (!this.tx.status.confirmed || this.waitingForAccelerationInfo)) || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id))));
|
||||
if (this.isAcceleration) {
|
||||
if (initialState) {
|
||||
this.accelerationFlowCompleted = true;
|
||||
@ -809,6 +836,21 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.miningStats = stats;
|
||||
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
|
||||
});
|
||||
if (!this.tx.status?.confirmed) {
|
||||
this.standardETA$ = combineLatest([
|
||||
this.stateService.mempoolBlocks$.pipe(startWith(null)),
|
||||
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
|
||||
]).pipe(
|
||||
map(([mempoolBlocks, da]) => {
|
||||
return this.etaService.calculateUnacceleratedETA(
|
||||
this.tx,
|
||||
mempoolBlocks,
|
||||
da,
|
||||
this.cpfpInfo,
|
||||
);
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
this.isAccelerated$.next(this.isAcceleration);
|
||||
}
|
||||
@ -864,6 +906,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
resetTransaction() {
|
||||
this.firstLoad = false;
|
||||
this.gotInitialPosition = false;
|
||||
this.error = undefined;
|
||||
this.tx = null;
|
||||
this.txChanged$.next(true);
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<app-truncate [text]="tx.txid"></app-truncate>
|
||||
</a>
|
||||
<div>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}</ng-template>
|
||||
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
|
||||
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
|
||||
</ng-template>
|
||||
|
||||
@ -9017,18 +9017,89 @@ export const restApiDocsData = [
|
||||
"effectiveFee": 154,
|
||||
"ancestorCount": 1
|
||||
},
|
||||
"cost": 3850,
|
||||
"targetFeeRate": 26,
|
||||
"nextBlockFee": 4004,
|
||||
"userBalance": 99900000,
|
||||
"mempoolBaseFee": 40000,
|
||||
"vsizeFee": 50000,
|
||||
"hasAccess": true
|
||||
"cost": 1386,
|
||||
"targetFeeRate": 10,
|
||||
"nextBlockFee": 1540,
|
||||
"userBalance": 0,
|
||||
"mempoolBaseFee": 50000,
|
||||
"vsizeFee": 0,
|
||||
"pools": [
|
||||
111,
|
||||
102,
|
||||
112,
|
||||
142,
|
||||
115
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"fee": 1500
|
||||
},
|
||||
{
|
||||
"fee": 3000
|
||||
},
|
||||
{
|
||||
"fee": 12500
|
||||
}
|
||||
],
|
||||
"hasAccess": false,
|
||||
"availablePaymentMethods": {
|
||||
"bitcoin": {
|
||||
"enabled": true,
|
||||
"min": 1000,
|
||||
"max": 10000000
|
||||
},
|
||||
"cashapp": {
|
||||
"enabled": true,
|
||||
"min": 10,
|
||||
"max": 200
|
||||
}
|
||||
},
|
||||
"unavailable": false
|
||||
}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
options: { officialOnly: true },
|
||||
type: "endpoint",
|
||||
category: "accelerator-public",
|
||||
httpRequestMethod: "POST",
|
||||
fragment: "accelerator-get-invoice",
|
||||
title: "POST Generate Acceleration Invoice",
|
||||
description: {
|
||||
default: "<p>Request a LN invoice to accelerate a transaction.</p>"
|
||||
},
|
||||
urlString: "/v1/services/payments/bitcoin",
|
||||
showConditions: [""],
|
||||
showJsExamples: showJsExamplesDefaultFalse,
|
||||
codeExample: {
|
||||
default: {
|
||||
codeTemplate: {
|
||||
curl: `%{1}" "[[hostname]][[baseNetworkUrl]]/api/v1/services/payments/bitcoin`, //custom interpolation technique handled in replaceCurlPlaceholder()
|
||||
commonJS: ``,
|
||||
esModule: ``
|
||||
},
|
||||
codeSampleMainnet: {
|
||||
esModule: [],
|
||||
commonJS: [],
|
||||
curl: ["product=ee13ebb99632377c15c94980357f674d285ac413452050031ea6dcd3e9b2dc29&amount=12500"],
|
||||
headers: "",
|
||||
response: `[
|
||||
{
|
||||
"btcpayInvoiceId": "4Ww53d7VgSa596jmCFufe7",
|
||||
"btcDue": "0.000625",
|
||||
"addresses": {
|
||||
"BTC": "bc1qcvqx2kr5mktd7gvym0atrrx0sn27mwv5kkghl3m78kegndm5t8ksvcqpja",
|
||||
"BTC_LNURLPAY": null,
|
||||
"BTC_LightningLike": "lnbc625u1pngl0wzpp56j7cqghsw2y5q7vdu9shmpxgpzsx4pqra4wcm9vdnvqegutplk2qdxj2pskjepqw3hjqnt9d4cx7mmvypqkxcm9d3jhyct5daezq2z0wfjx2u3qf9zr5grpvd3k2mr9wfshg6t0dckk2ef3xdjkyc3e8ymrxv3nxumkxvf4vvungwfcxqen2dmxxcmngepj8q6kzce5xyengdfjxq6nqvpnx9jkzdnyvdjrxefevgexgcej8yknzdejxqmrjd3jx5mrgdpj9ycqzpuxqrpr5sp58593dzj2uauaj3afa7x47qeam8k9yyqrh9qasj2ssdzstew6qv3q9qxpqysgqj8qshfkxmj0gfkly5xfydysvsx55uhnc6fgpw66uf6hl8leu07454axe2kq0q788yysg8guel2r36d6f75546nkhmdcmec4mmlft8dsq62rnsj"
|
||||
}
|
||||
}
|
||||
]`,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
options: { officialOnly: true },
|
||||
type: "endpoint",
|
||||
@ -9119,14 +9190,18 @@ export const restApiDocsData = [
|
||||
"txid": "d7e1796d8eb4a09d4e6c174e36cfd852f1e6e6c9f7df4496339933cd32cbdd1d",
|
||||
"status": "completed",
|
||||
"added": 1707421053,
|
||||
"lastUpdated": 1707422952,
|
||||
"lastUpdated": 1719134667,
|
||||
"effectiveFee": 146,
|
||||
"effectiveVsize": 141,
|
||||
"feeDelta": 14000,
|
||||
"blockHash": "00000000000000000000482f0746d62141694b9210a813b97eb8445780a32003",
|
||||
"blockHeight": 829559,
|
||||
"bidBoost": 6102,
|
||||
"pools": [111]
|
||||
"bidBoost": 3239,
|
||||
"boostVersion": "v1",
|
||||
"pools": [
|
||||
111
|
||||
],
|
||||
"minedByPoolUniqueId": 111
|
||||
}
|
||||
]`,
|
||||
},
|
||||
@ -9229,7 +9304,7 @@ export const restApiDocsData = [
|
||||
category: "accelerator-private",
|
||||
httpRequestMethod: "POST",
|
||||
fragment: "accelerator-accelerate",
|
||||
title: "POST Accelerate A Transaction",
|
||||
title: "POST Accelerate A Transaction (Pro)",
|
||||
description: {
|
||||
default: "<p>Sends a request to accelerate a transaction.</p>"
|
||||
},
|
||||
|
||||
@ -194,7 +194,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-to-get-transaction-confirmed-quickly">
|
||||
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p *ngIf="officialMempoolInstance">Another option to get your transaction confirmed more quickly is Mempool Accelerator™. This service is still in development, but you can <a href="https://mempool.space/accelerator">sign up for the waitlist</a> to be notified when it's ready.</p>
|
||||
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p>Another option to get your transaction confirmed more quickly is <a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">Mempool Accelerator™</a>.</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template type="how-prevent-stuck-transaction">
|
||||
|
||||
@ -33,6 +33,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
showMobileEnterpriseUpsell: boolean = true;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
isMempoolSpaceBuild = this.stateService.isMempoolSpaceBuild;
|
||||
|
||||
@ViewChildren(FaqTemplateDirective) faqTemplates: QueryList<FaqTemplateDirective>;
|
||||
dict = {};
|
||||
|
||||
@ -50,7 +50,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'acceleration',
|
||||
data: { networks: ['bitcoin'] },
|
||||
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@ -61,7 +61,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'acceleration/list/:page',
|
||||
data: { networks: ['bitcoin'] },
|
||||
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
|
||||
component: AccelerationsListComponent,
|
||||
},
|
||||
{
|
||||
@ -140,7 +140,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'acceleration/fees',
|
||||
data: { networks: ['bitcoin'] },
|
||||
data: { networks: ['bitcoin'], networkSpecific: true, onlySubnet: [''] },
|
||||
component: AccelerationFeesGraphComponent,
|
||||
},
|
||||
{
|
||||
|
||||
@ -411,7 +411,7 @@ export interface Acceleration {
|
||||
}
|
||||
|
||||
export interface AccelerationHistoryParams {
|
||||
status?: string;
|
||||
status?: string; // Single status or comma separated list of status
|
||||
timeframe?: string;
|
||||
poolUniqueId?: number;
|
||||
blockHash?: string;
|
||||
|
||||
@ -5,6 +5,7 @@ export type MenuItem = {
|
||||
i18n: string;
|
||||
faIcon: IconName;
|
||||
link: string;
|
||||
isExternal?: boolean;
|
||||
};
|
||||
export type MenuGroup = {
|
||||
title: string;
|
||||
|
||||
@ -8,6 +8,7 @@ import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { Conversion } from './price.service';
|
||||
import { StorageService } from './storage.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { TxAuditStatus } from '../components/transaction/transaction.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -17,6 +18,7 @@ export class ApiService {
|
||||
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
public blockAuditLoaded: { [hash: string]: boolean } = {};
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
@ -369,11 +371,18 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getBlockAudit$(hash: string) : Observable<BlockAudit> {
|
||||
this.setBlockAuditLoaded(hash);
|
||||
return this.httpClient.get<BlockAudit>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/audit-summary`
|
||||
);
|
||||
}
|
||||
|
||||
getBlockTxAudit$(hash: string, txid: string) : Observable<TxAuditStatus> {
|
||||
return this.httpClient.get<TxAuditStatus>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/block/${hash}/tx/${txid}/audit`
|
||||
);
|
||||
}
|
||||
|
||||
getBlockAuditScores$(from: number): Observable<AuditScore[]> {
|
||||
return this.httpClient.get<AuditScore[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
|
||||
@ -526,4 +535,13 @@ export class ApiService {
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/accelerations/total' + (queryString?.length ? '?' + queryString : '')
|
||||
);
|
||||
}
|
||||
|
||||
// Cache methods
|
||||
async setBlockAuditLoaded(hash: string) {
|
||||
this.blockAuditLoaded[hash] = true;
|
||||
}
|
||||
|
||||
getBlockAuditLoaded(hash) {
|
||||
return this.blockAuditLoaded[hash];
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,7 @@ export class CacheService {
|
||||
resetBlockCache() {
|
||||
this.blockHashCache = {};
|
||||
this.blockCache = {};
|
||||
this.apiService.blockAuditLoaded = {};
|
||||
this.blockLoading = {};
|
||||
this.copiesInBlockQueue = {};
|
||||
this.blockPriorities = [];
|
||||
|
||||
@ -225,4 +225,58 @@ export class EtaService {
|
||||
blocks: Math.ceil(eta / da.adjustedTimeAvg),
|
||||
};
|
||||
}
|
||||
|
||||
calculateUnacceleratedETA(
|
||||
tx: Transaction,
|
||||
mempoolBlocks: MempoolBlock[],
|
||||
da: DifficultyAdjustment,
|
||||
cpfpInfo: CpfpInfo | null,
|
||||
): ETA | null {
|
||||
if (!tx || !mempoolBlocks) {
|
||||
return null;
|
||||
}
|
||||
const now = Date.now();
|
||||
|
||||
// use known projected position, or fall back to feerate-based estimate
|
||||
const mempoolPosition = this.mempoolPositionFromFees(this.getFeeRateFromCpfpInfo(tx, cpfpInfo), mempoolBlocks);
|
||||
if (!mempoolPosition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// difficulty adjustment estimate is required to know avg block time on non-Liquid networks
|
||||
if (!da) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const blocks = mempoolPosition.block + 1;
|
||||
const wait = da.adjustedTimeAvg * (mempoolPosition.block + 1);
|
||||
return {
|
||||
now,
|
||||
time: wait + now + da.timeOffset,
|
||||
wait,
|
||||
blocks,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
getFeeRateFromCpfpInfo(tx: Transaction, cpfpInfo: CpfpInfo | null): number {
|
||||
if (!cpfpInfo) {
|
||||
return tx.fee / (tx.weight / 4);
|
||||
}
|
||||
|
||||
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||
relatives.push(cpfpInfo.bestDescendant);
|
||||
}
|
||||
|
||||
if (!!relatives.length) {
|
||||
const totalWeight = tx.weight + relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
const totalFees = tx.fee + relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
|
||||
return totalFees / (totalWeight / 4);
|
||||
}
|
||||
|
||||
return tx.fee / (tx.weight / 4);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StateService } from './state.service';
|
||||
import { RelativeUrlPipe } from '../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -30,15 +31,30 @@ export class NavigationService {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.snapshot.root),
|
||||
).subscribe((state) => {
|
||||
this.updateSubnetPaths(state);
|
||||
if (this.enforceSubnetRestrictions(state)) {
|
||||
this.updateSubnetPaths(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enforceSubnetRestrictions(root: ActivatedRouteSnapshot): boolean {
|
||||
let route = root;
|
||||
while (route) {
|
||||
if (route.data.onlySubnet && !route.data.onlySubnet.includes(this.stateService.network)) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('')]);
|
||||
return false;
|
||||
}
|
||||
route = route.firstChild;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// For each network (bitcoin/liquid), find and save the longest url path compatible with the current route
|
||||
updateSubnetPaths(root: ActivatedRouteSnapshot): void {
|
||||
let path = '';
|
||||
|
||||
@ -25,9 +25,6 @@ export interface IUser {
|
||||
ogRank: number | null;
|
||||
}
|
||||
|
||||
// Todo - move to config.json
|
||||
const SERVICES_API_PREFIX = `/api/v1/services`;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@ -98,7 +95,7 @@ export class ServicesApiServices {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/account`);
|
||||
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/account`);
|
||||
}
|
||||
|
||||
getUserMenuGroups$(): Observable<MenuGroup[]> {
|
||||
@ -107,7 +104,7 @@ export class ServicesApiServices {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.httpClient.get<MenuGroup[]>(`${SERVICES_API_PREFIX}/account/menu`);
|
||||
return this.httpClient.get<MenuGroup[]>(`${this.stateService.env.SERVICES_API}/account/menu`);
|
||||
}
|
||||
|
||||
logout$(): Observable<any> {
|
||||
@ -117,59 +114,59 @@ export class ServicesApiServices {
|
||||
}
|
||||
|
||||
localStorage.removeItem('auth');
|
||||
return this.httpClient.post(`${SERVICES_API_PREFIX}/auth/logout`, {});
|
||||
return this.httpClient.post(`${this.stateService.env.SERVICES_API}/auth/logout`, {});
|
||||
}
|
||||
|
||||
getJWT$() {
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/auth/getJWT`);
|
||||
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/auth/getJWT`);
|
||||
}
|
||||
|
||||
getServicesBackendInfo$(): Observable<IBackendInfo> {
|
||||
return this.httpClient.get<IBackendInfo>(`${SERVICES_API_PREFIX}/version`);
|
||||
return this.httpClient.get<IBackendInfo>(`${this.stateService.env.SERVICES_API}/version`);
|
||||
}
|
||||
|
||||
estimate$(txInput: string) {
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
|
||||
}
|
||||
|
||||
accelerate$(txInput: string, userBid: number, accelerationUUID: string) {
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
|
||||
}
|
||||
|
||||
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) {
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID });
|
||||
}
|
||||
|
||||
getAccelerations$(): Observable<Acceleration[]> {
|
||||
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations`);
|
||||
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations`);
|
||||
}
|
||||
|
||||
getAggregatedAccelerationHistory$(params: AccelerationHistoryParams): Observable<any> {
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history/aggregated`, { params: { ...params }, observe: 'response' });
|
||||
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history/aggregated`, { params: { ...params }, observe: 'response' });
|
||||
}
|
||||
|
||||
getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> {
|
||||
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } });
|
||||
return this.httpClient.get<Acceleration[]>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } });
|
||||
}
|
||||
|
||||
getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> {
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
|
||||
return this.httpClient.get<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
|
||||
}
|
||||
|
||||
getAccelerationStats$(params: AccelerationHistoryParams): Observable<AccelerationStats> {
|
||||
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`, { params: { ...params } });
|
||||
return this.httpClient.get<AccelerationStats>(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/stats`, { params: { ...params } });
|
||||
}
|
||||
|
||||
setupSquare$(): Observable<{squareAppId: string, squareLocationId: string}> {
|
||||
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${SERVICES_API_PREFIX}/square/setup`);
|
||||
return this.httpClient.get<{squareAppId: string, squareLocationId: string}>(`${this.stateService.env.SERVICES_API}/square/setup`);
|
||||
}
|
||||
|
||||
getFaucetStatus$() {
|
||||
return this.httpClient.get<{ address?: string, min: number, max: number, code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'}>(`${SERVICES_API_PREFIX}/testnet4/faucet/status`, { responseType: 'json' });
|
||||
return this.httpClient.get<{ address?: string, min: number, max: number, code: 'ok' | 'faucet_not_available' | 'faucet_maximum_reached' | 'faucet_too_soon'}>(`${this.stateService.env.SERVICES_API}/testnet4/faucet/status`, { responseType: 'json' });
|
||||
}
|
||||
|
||||
requestTestnet4Coins$(address: string, sats: number) {
|
||||
return this.httpClient.get<{txid: string}>(`${SERVICES_API_PREFIX}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
|
||||
return this.httpClient.get<{txid: string}>(`${this.stateService.env.SERVICES_API}/testnet4/faucet/request?address=${address}&sats=${sats}`, { responseType: 'json' });
|
||||
}
|
||||
|
||||
generateBTCPayAcceleratorInvoice$(txid: string, sats: number): Observable<any> {
|
||||
@ -177,14 +174,14 @@ export class ServicesApiServices {
|
||||
product: txid,
|
||||
amount: sats,
|
||||
};
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/payments/bitcoin`, params);
|
||||
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin`, params);
|
||||
}
|
||||
|
||||
retreiveInvoice$(invoiceId: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/invoice?id=${invoiceId}`);
|
||||
return this.httpClient.get<any[]>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/invoice?id=${invoiceId}`);
|
||||
}
|
||||
|
||||
getPaymentStatus$(orderId: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(`${SERVICES_API_PREFIX}/payments/bitcoin/check?order_id=${orderId}`);
|
||||
return this.httpClient.get<any[]>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/check?order_id=${orderId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,7 @@ export interface Env {
|
||||
ADDITIONAL_CURRENCIES: boolean;
|
||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||
SERVICES_API?: string;
|
||||
customize?: Customization;
|
||||
}
|
||||
|
||||
@ -109,6 +110,7 @@ const defaultEnv: Env = {
|
||||
'ACCELERATOR': false,
|
||||
'PUBLIC_ACCELERATIONS': false,
|
||||
'ADDITIONAL_CURRENCIES': false,
|
||||
'SERVICES_API': 'https://mempool.space/api/v1/services',
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
|
||||
@ -6,4 +6,4 @@
|
||||
</span>
|
||||
}
|
||||
|
||||
<ng-template #lowBalance i18n="accelerator.low-balance">Your balance is too low.<br/>Please <a style="color:#105fb0" href="/services/accelerator/overview">top up your account</a>.</ng-template>
|
||||
<ng-template #lowBalance i18n="accelerator.low-balance">Your balance is too low.<br/>Please <a class="top-up-link" href="/services/accelerator/overview">top up your account</a>.</ng-template>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ You can also have the mempool.space team run a highly-performant and highly-avai
|
||||
|
||||
### Server Hardware
|
||||
|
||||
Mempool v2 is powered by [blockstream/electrs](https://github.com/Blockstream/electrs), which is a beast.
|
||||
Mempool v3 is powered by [mempool/electrs](https://github.com/mempool/electrs), which is a beast.
|
||||
|
||||
I recommend a beefy server:
|
||||
|
||||
|
||||
@ -47,9 +47,6 @@ class Server {
|
||||
case "liquid":
|
||||
canonical = "https://liquid.network"
|
||||
break;
|
||||
case "bisq":
|
||||
canonical = "https://bisq.markets"
|
||||
break;
|
||||
case "onbtc":
|
||||
canonical = "https://bitcoin.gob.sv"
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user