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