From 104c7f428534971f294c623042d59d49f3b7ba4b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 8 Aug 2024 13:12:31 +0000 Subject: [PATCH 01/73] Persist mempool block visualization between pages --- .../block-overview-graph.component.ts | 4 +-- .../block-overview-graph/block-scene.ts | 11 +++--- .../mempool-block-overview.component.ts | 23 +++++++++++-- .../mempool-block/mempool-block.component.ts | 2 +- .../src/app/interfaces/websocket.interface.ts | 2 ++ frontend/src/app/services/state.service.ts | 34 +++++++++++++------ .../src/app/services/websocket.service.ts | 24 ++++++++++--- frontend/src/app/shared/common.utils.ts | 3 +- 8 files changed, 76 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index ab9a29293..3be0692a5 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -198,7 +198,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } // initialize the scene without any entry transition - setup(transactions: TransactionStripped[]): void { + setup(transactions: TransactionStripped[], sort: boolean = false): void { const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false); if (filtersAvailable !== this.filtersAvailable) { this.setFilterFlags(); @@ -206,7 +206,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.filtersAvailable = filtersAvailable; if (this.scene) { this.clearUpdateQueue(); - this.scene.setup(transactions); + this.scene.setup(transactions, sort); this.readyNextFrame = true; this.start(); this.updateSearchHighlight(); diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index c59fcb7d4..4f07818a5 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -88,16 +88,19 @@ export default class BlockScene { } // set up the scene with an initial set of transactions, without any transition animation - setup(txs: TransactionStripped[]) { + setup(txs: TransactionStripped[], sort: boolean = false) { // clean up any old transactions Object.values(this.txs).forEach(tx => { tx.destroy(); delete this.txs[tx.txid]; }); this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); - txs.forEach(tx => { - const txView = new TxView(tx, this); - this.txs[tx.txid] = txView; + let txViews = txs.map(tx => new TxView(tx, this)); + if (sort) { + txViews = txViews.sort(feeRateDescending); + } + txViews.forEach(txView => { + this.txs[txView.txid] = txView; this.place(txView); this.saveGridToScreenPosition(txView); this.applyTxUpdate(txView, { diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 2c564882e..50f8b650f 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -31,7 +31,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang lastBlockHeight: number; blockIndex: number; - isLoading$ = new BehaviorSubject(true); + isLoading$ = new BehaviorSubject(false); timeLtrSubscription: Subscription; timeLtr: boolean; chainDirection: string = 'right'; @@ -95,6 +95,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang } } this.updateBlock({ + block: this.blockIndex, removed, changed, added @@ -110,8 +111,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang if (this.blockGraph) { this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); } - this.isLoading$.next(true); - this.websocketService.startTrackMempoolBlock(changes.index.currentValue); + if (!this.websocketService.startTrackMempoolBlock(changes.index.currentValue) && this.stateService.mempoolBlockState && this.stateService.mempoolBlockState.block === changes.index.currentValue) { + this.resumeBlock(Object.values(this.stateService.mempoolBlockState.transactions)); + } else { + this.isLoading$.next(true); + } } } @@ -153,6 +157,19 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang this.isLoading$.next(false); } + resumeBlock(transactionsStripped: TransactionStripped[]): void { + if (this.blockGraph) { + this.firstLoad = false; + this.blockGraph.setup(transactionsStripped, true); + this.blockIndex = this.index; + this.isLoading$.next(false); + } else { + requestAnimationFrame(() => { + this.resumeBlock(transactionsStripped); + }); + } + } + onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void { const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`); if (!event.keyModifier) { diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts index 430a456ec..d2e658302 100644 --- a/frontend/src/app/components/mempool-block/mempool-block.component.ts +++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts @@ -71,7 +71,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy { }) ); - this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap))); + this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(({transactions}) => Object.values(transactions))); this.network$ = this.stateService.networkChanged$; } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 35e0ffa09..7552224f5 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -72,11 +72,13 @@ export interface MempoolBlockWithTransactions extends MempoolBlock { } export interface MempoolBlockDelta { + block: number; added: TransactionStripped[]; removed: string[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[]; } export interface MempoolBlockState { + block: number; transactions: TransactionStripped[]; } export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 365c1daa2..13ffc7fc5 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -5,7 +5,7 @@ import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, Mempool import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; -import { filter, map, scan, shareReplay } from 'rxjs/operators'; +import { filter, map, scan, share, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; import { ActiveFilter } from '../shared/filters.utils'; @@ -131,6 +131,7 @@ export class StateService { latestBlockHeight = -1; blocks: BlockExtended[] = []; mempoolSequence: number; + mempoolBlockState: { block: number, transactions: { [txid: string]: TransactionStripped} }; backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora'); networkChanged$ = new ReplaySubject(1); @@ -143,7 +144,7 @@ export class StateService { mempoolInfo$ = new ReplaySubject(1); mempoolBlocks$ = new ReplaySubject(1); mempoolBlockUpdate$ = new Subject(); - liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; + liveMempoolBlockTransactions$: Observable<{ block: number, transactions: { [txid: string]: TransactionStripped} }>; accelerations$ = new Subject(); liveAccelerations$: Observable; txConfirmed$ = new Subject<[string, BlockExtended]>(); @@ -231,29 +232,40 @@ export class StateService { } }); - this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => { + this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((acc: { block: number, transactions: { [txid: string]: TransactionStripped } }, change: MempoolBlockUpdate): { block: number, transactions: { [txid: string]: TransactionStripped } } => { if (isMempoolState(change)) { const txMap = {}; change.transactions.forEach(tx => { txMap[tx.txid] = tx; }); - return txMap; + this.mempoolBlockState = { + block: change.block, + transactions: txMap + }; + return this.mempoolBlockState; } else { change.added.forEach(tx => { - transactions[tx.txid] = tx; + acc.transactions[tx.txid] = tx; }); change.removed.forEach(txid => { - delete transactions[txid]; + delete acc.transactions[txid]; }); change.changed.forEach(tx => { - if (transactions[tx.txid]) { - transactions[tx.txid].rate = tx.rate; - transactions[tx.txid].acc = tx.acc; + if (acc.transactions[tx.txid]) { + acc.transactions[tx.txid].rate = tx.rate; + acc.transactions[tx.txid].acc = tx.acc; } }); - return transactions; + this.mempoolBlockState = { + block: change.block, + transactions: acc.transactions + }; + return this.mempoolBlockState; } - }, {})); + }, {}), + share() + ); + this.liveMempoolBlockTransactions$.subscribe(); // Emits the full list of pending accelerations each time it changes this.liveAccelerations$ = this.accelerations$.pipe( diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index fd67ddb2e..39e9d1af3 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -35,6 +35,7 @@ export class WebsocketService { private isTrackingAddresses: string[] | false = false; private isTrackingAccelerations: boolean = false; private trackingMempoolBlock: number; + private stoppingTrackMempoolBlock: any | null = null; private latestGitCommit = ''; private onlineCheckTimeout: number; private onlineCheckTimeoutTwo: number; @@ -203,19 +204,31 @@ export class WebsocketService { this.websocketSubject.next({ 'track-asset': 'stop' }); } - startTrackMempoolBlock(block: number, force: boolean = false) { + startTrackMempoolBlock(block: number, force: boolean = false): boolean { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } // skip duplicate tracking requests if (force || this.trackingMempoolBlock !== block) { this.websocketSubject.next({ 'track-mempool-block': block }); this.isTrackingMempoolBlock = true; this.trackingMempoolBlock = block; + return true; } + return false; } - stopTrackMempoolBlock() { - this.websocketSubject.next({ 'track-mempool-block': -1 }); + stopTrackMempoolBlock(): void { + if (this.stoppingTrackMempoolBlock) { + clearTimeout(this.stoppingTrackMempoolBlock); + } this.isTrackingMempoolBlock = false; - this.trackingMempoolBlock = null; + this.stoppingTrackMempoolBlock = setTimeout(() => { + this.stoppingTrackMempoolBlock = null; + this.websocketSubject.next({ 'track-mempool-block': -1 }); + this.trackingMempoolBlock = null; + this.stateService.mempoolBlockState = null; + }, 2000); } startTrackRbf(mode: 'all' | 'fullRbf') { @@ -424,6 +437,7 @@ export class WebsocketService { if (response['projected-block-transactions'].blockTransactions) { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; this.stateService.mempoolBlockUpdate$.next({ + block: this.trackingMempoolBlock, transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), }); } else if (response['projected-block-transactions'].delta) { @@ -432,7 +446,7 @@ export class WebsocketService { this.startTrackMempoolBlock(this.trackingMempoolBlock, true); } else { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta)); } } } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 697b11b5e..8c69c2319 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -170,8 +170,9 @@ export function uncompressTx(tx: TransactionCompressed): TransactionStripped { }; } -export function uncompressDeltaChange(delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { +export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCompressed): MempoolBlockDelta { return { + block, added: delta.added.map(uncompressTx), removed: delta.removed, changed: delta.changed.map(tx => ({ From 79e494150c19f6f30ce5cf23ef2baa0d1a67945e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 9 Aug 2024 14:44:51 +0000 Subject: [PATCH 02/73] fix mined acceleration detection logic on tx pages --- frontend/src/app/components/tracker/tracker.component.ts | 2 +- .../src/app/components/transaction/transaction.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index c869f9705..24b5fc1dc 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -293,7 +293,7 @@ export class TrackerComponent implements OnInit, OnDestroy { }) ).subscribe((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') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { const boostCost = acceleration.boostCost || acceleration.bidBoost; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.boost = boostCost; diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index bcad164cc..01bbcb6f4 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -358,7 +358,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }), ).subscribe((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') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { const boostCost = acceleration.boostCost || acceleration.bidBoost; acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; acceleration.boost = boostCost; From a31729b8b8ffaf611cd2274954d7abd2cc419e79 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 10 Aug 2024 21:56:11 +0000 Subject: [PATCH 03/73] fix feeDelta display logic --- .../transaction/transaction.component.html | 19 +++++++++---------- .../transaction/transaction.component.ts | 14 ++++++++------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index ecd00e599..2ae6c8df8 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -606,16 +606,15 @@ @if (!isLoadingTx) { Fee - {{ tx.fee | number }} sat - @if (accelerationInfo?.bidBoost) { - +{{ accelerationInfo.bidBoost | number }} sat - - } @else if (tx.feeDelta && !accelerationInfo) { - +{{ tx.feeDelta | number }} sat - - } @else { - - } + {{ tx.fee | number }} sat + + @if (accelerationInfo?.bidBoost) { + +{{ accelerationInfo.bidBoost | number }} sat + } @else if (tx.feeDelta) { + +{{ tx.feeDelta | number }} sat + } + + } @else { diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 01bbcb6f4..637aa52e3 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -358,12 +358,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { }), ).subscribe((accelerationHistory) => { for (const acceleration of accelerationHistory) { - if (acceleration.txid === this.txId && (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { - const boostCost = acceleration.boostCost || acceleration.bidBoost; - acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - acceleration.boost = boostCost; - this.tx.acceleratedAt = acceleration.added; - this.accelerationInfo = acceleration; + if (acceleration.txid === this.txId) { + if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + const boostCost = acceleration.boostCost || acceleration.bidBoost; + acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + acceleration.boost = boostCost; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } this.waitingForAccelerationInfo = false; this.setIsAccelerated(); } From ce4b0ed0f3aa6fc831bc5c18f18894011f46411f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 10 Aug 2024 21:57:31 +0000 Subject: [PATCH 04/73] Implement v1 audit in tx audit API --- backend/src/repositories/BlocksAuditsRepository.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/repositories/BlocksAuditsRepository.ts b/backend/src/repositories/BlocksAuditsRepository.ts index abf26aa29..3b3f79ce0 100644 --- a/backend/src/repositories/BlocksAuditsRepository.ts +++ b/backend/src/repositories/BlocksAuditsRepository.ts @@ -132,11 +132,12 @@ class BlocksAuditRepositories { firstSeen = tx.time; } }); + const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated); return { - seen: isExpected || isPrioritized || isAccelerated, + seen: wasSeen, expected: isExpected, - added: isAdded, + added: isAdded && (blockAudit.version === 0 || !wasSeen), prioritized: isPrioritized, conflict: isConflict, accelerated: isAccelerated, From b8cfeb579b7c9c2e41cd651d8a3a5bc6a4cdbc5b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 11 Aug 2024 20:38:54 +0000 Subject: [PATCH 05/73] make accelerations magical again --- .../acceleration-sparkles.component.html | 5 ++ .../acceleration-sparkles.component.scss | 45 ++++++++++++ .../acceleration-sparkles.component.ts | 71 +++++++++++++++++++ .../mempool-blocks.component.html | 3 +- .../mempool-blocks.component.ts | 12 +++- frontend/src/app/shared/shared.module.ts | 3 + 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html create mode 100644 frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss create mode 100644 frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html new file mode 100644 index 000000000..bf0080344 --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.html @@ -0,0 +1,5 @@ +
+
+ + +
+
\ No newline at end of file diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss new file mode 100644 index 000000000..35f6e32d5 --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.scss @@ -0,0 +1,45 @@ +.sparkles { + position: absolute; + top: var(--block-size); + height: 50px; + right: 0; +} + +.sparkle { + position: absolute; + color: rgba(152, 88, 255, 0.75); + opacity: 0; + transform: scale(0.8) rotate(0deg); + animation: pop ease 2000ms forwards, sparkle ease 500ms infinite; +} + +.inner-sparkle { + display: block; +} + +@keyframes pop { + 0% { + transform: scale(0.8) rotate(0deg); + opacity: 0; + } + 20% { + transform: scale(1) rotate(72deg); + opacity: 1; + } + 100% { + transform: scale(0) rotate(360deg); + opacity: 0; + } +} + +@keyframes sparkle { + 0% { + color: rgba(152, 88, 255, 0.75); + } + 50% { + color: rgba(198, 162, 255, 0.75); + } + 100% { + color: rgba(152, 88, 255, 0.75); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts new file mode 100644 index 000000000..bde7eb8ed --- /dev/null +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts @@ -0,0 +1,71 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-acceleration-sparkles', + templateUrl: './acceleration-sparkles.component.html', + styleUrls: ['./acceleration-sparkles.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AccelerationSparklesComponent implements OnChanges { + @Input() arrow: ElementRef; + @Input() run: boolean = false; + + @ViewChild('sparkleAnchor') + sparkleAnchor: ElementRef; + + constructor( + private cd: ChangeDetectorRef, + ) {} + + endTimeout: any; + lastSparkle: number = 0; + sparkleWidth: number = 0; + sparkles: any[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if (changes.run) { + if (this.endTimeout) { + clearTimeout(this.endTimeout); + this.endTimeout = null; + } + if (this.run) { + this.doSparkle(); + } else { + this.endTimeout = setTimeout(() => { + this.sparkles = []; + }, 2000); + } + } + } + + doSparkle(): void { + if (this.run) { + const now = performance.now(); + if (now - this.lastSparkle > 30) { + this.lastSparkle = now; + if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) { + const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right; + const right = this.arrow.nativeElement.getBoundingClientRect().right; + const dx = (anchor - right) + 37.5; + this.sparkles.push({ + style: { + right: dx + 'px', + top: (Math.random() * 30) + 'px', + animationDelay: (Math.random() * 50) + 'ms', + }, + rotation: { + transform: `rotate(${Math.random() * 360}deg)`, + } + }); + while (this.sparkles.length > 100) { + this.sparkles.shift(); + } + this.cd.markForCheck(); + } + } + requestAnimationFrame(() => { + this.doSparkle(); + }); + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html index 24f229598..b979e032b 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.html @@ -51,7 +51,8 @@ -
+ +
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 13608bb73..a0958ec40 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'; import { Subscription, Observable, of, combineLatest } from 'rxjs'; import { MempoolBlock } from '../../interfaces/websocket.interface'; import { StateService } from '../../services/state.service'; @@ -77,6 +77,9 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { maxArrowPosition = 0; rightPosition = 0; transition = 'background 2s, right 2s, transform 1s'; + @ViewChild('arrowUp') + arrowElement: ElementRef; + acceleratingArrow: boolean = false; markIndex: number; txPosition: MempoolPosition; @@ -201,6 +204,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.markBlocksSubscription = this.stateService.markBlock$ .subscribe((state) => { + const oldTxPosition = this.txPosition; this.markIndex = undefined; this.txPosition = undefined; this.txFeePerVSize = undefined; @@ -209,6 +213,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } if (state.mempoolPosition) { this.txPosition = state.mempoolPosition; + if (this.txPosition.accelerated && !oldTxPosition.accelerated) { + this.acceleratingArrow = true; + setTimeout(() => { + this.acceleratingArrow = false; + }, 2000); + } } if (state.txFeePerVSize) { this.txFeePerVSize = state.txFeePerVSize; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 89bcfafbb..2d5b4d0f9 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -100,6 +100,7 @@ import { MempoolErrorComponent } from './components/mempool-error/mempool-error. import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; +import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component'; import { BlockViewComponent } from '../components/block-view/block-view.component'; import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; @@ -225,6 +226,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationsListComponent, AccelerationStatsComponent, PendingStatsComponent, + AccelerationSparklesComponent, HttpErrorComponent, TwitterWidgetComponent, FaucetComponent, @@ -355,6 +357,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationsListComponent, AccelerationStatsComponent, PendingStatsComponent, + AccelerationSparklesComponent, HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, From 021f0b32a13e97b317781be523502ee96a81efed Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 11 Aug 2024 20:52:26 +0000 Subject: [PATCH 06/73] sparklier sparkles --- .../acceleration-sparkles.component.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts index bde7eb8ed..2316c996d 100644 --- a/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts +++ b/frontend/src/app/components/acceleration/sparkles/acceleration-sparkles.component.ts @@ -41,23 +41,25 @@ export class AccelerationSparklesComponent implements OnChanges { doSparkle(): void { if (this.run) { const now = performance.now(); - if (now - this.lastSparkle > 30) { + if (now - this.lastSparkle > 20) { this.lastSparkle = now; if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) { const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right; const right = this.arrow.nativeElement.getBoundingClientRect().right; - const dx = (anchor - right) + 37.5; - this.sparkles.push({ - style: { - right: dx + 'px', - top: (Math.random() * 30) + 'px', - animationDelay: (Math.random() * 50) + 'ms', - }, - rotation: { - transform: `rotate(${Math.random() * 360}deg)`, - } - }); - while (this.sparkles.length > 100) { + const dx = (anchor - right) + 30; + const numSparkles = Math.ceil(Math.random() * 3); + for (let i = 0; i < numSparkles; i++) { + this.sparkles.push({ + style: { + right: (dx + (Math.random() * 10)) + 'px', + top: (15 + (Math.random() * 30)) + 'px', + }, + rotation: { + transform: `rotate(${Math.random() * 360}deg)`, + } + }); + } + while (this.sparkles.length > 200) { this.sparkles.shift(); } this.cd.markForCheck(); From ca26154426588c56f78476a931feaf0ed16d6fcf Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 11 Aug 2024 23:51:16 +0200 Subject: [PATCH 07/73] pull from transifex --- frontend/src/locale/messages.hr.xlf | 217 ++++++++++++++++++++++++++++ frontend/src/locale/messages.ko.xlf | 19 +++ 2 files changed, 236 insertions(+) diff --git a/frontend/src/locale/messages.hr.xlf b/frontend/src/locale/messages.hr.xlf index df4f5f4e0..5a37a7ff7 100644 --- a/frontend/src/locale/messages.hr.xlf +++ b/frontend/src/locale/messages.hr.xlf @@ -2806,6 +2806,7 @@ See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles. + Pogledajte bitcoin naknade vizualizirane tijekom vremena, uključujući minimalne i maksimalne naknade po bloku zajedno s naknadama na različitim postocima. src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 73 @@ -2813,6 +2814,7 @@ Block Fees + Naknade bloka src/app/components/block-fees-graph/block-fees-graph.component.html 6 @@ -2829,6 +2831,7 @@ See the average mining fees earned per Bitcoin block visualized in BTC and USD over time. + Pogledajte prosječne rudarske naknade zarađene po Bitcoin bloku vizualizirane u BTC-u i USD-u tijekom vremena. src/app/components/block-fees-graph/block-fees-graph.component.ts 70 @@ -2836,6 +2839,7 @@ Indexing blocks + Indeksiranje blokova src/app/components/block-fees-graph/block-fees-graph.component.ts 119 @@ -2871,6 +2875,7 @@ Block Fees Vs Subsidy + Blok naknade vs subsidy src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html 6 @@ -2887,6 +2892,7 @@ See the mining fees earned per Bitcoin block compared to the Bitcoin block subsidy, visualized in BTC and USD over time. + Pogledajte naknade za rudarenje zarađene po bloku Bitcoina u usporedbi sa Bitcoin blok subsidy, vizualizirane u BTC-u i USD-u tijekom vremena. src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts 79 @@ -2894,6 +2900,7 @@ At block + U bloku src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts 185 @@ -2901,6 +2908,7 @@ Around block + Oko bloka src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts 187 @@ -2908,6 +2916,7 @@ select filter categories to highlight matching transactions + odaberite kategorije filtera za označavanje odgovarajućih transakcija src/app/components/block-filters/block-filters.component.html 2 @@ -2916,6 +2925,7 @@ beta + beta src/app/components/block-filters/block-filters.component.html 3 @@ -2940,6 +2950,7 @@ Match + Podudaranje src/app/components/block-filters/block-filters.component.html 19 @@ -2952,6 +2963,7 @@ Any + Bilo koji src/app/components/block-filters/block-filters.component.html 25 @@ -2960,6 +2972,7 @@ Tint + Nijansa src/app/components/block-filters/block-filters.component.html 30 @@ -2968,6 +2981,7 @@ Classic + klasična src/app/components/block-filters/block-filters.component.html 33 @@ -2980,6 +2994,7 @@ Age + Dob src/app/components/block-filters/block-filters.component.html 36 @@ -2988,6 +3003,7 @@ Block Health + Zdravlje bloka src/app/components/block-health-graph/block-health-graph.component.html 6 @@ -3004,6 +3020,7 @@ See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm. + Pogledajte zdravlje Bitcoin bloka vizualizirano tijekom vremena. Zdravlje bloka mjera je koliko je očekivanih transakcija uključeno u stvarni izrudareni blok. Očekivane transakcije određuju se pomoću Mempoolove re-implementacije Bitcoin Core algoritma za odabir transakcija. src/app/components/block-health-graph/block-health-graph.component.ts 64 @@ -3011,6 +3028,7 @@ No data to display yet. Try again later. + Još nema podataka za prikaz. Pokušajte ponovno kasnije. src/app/components/block-health-graph/block-health-graph.component.ts 109 @@ -3034,6 +3052,7 @@ Health + Zdravlje src/app/components/block-health-graph/block-health-graph.component.ts 190 @@ -3057,6 +3076,7 @@ not available + nije dostupno src/app/components/block-overview-graph/block-overview-graph.component.html 7 @@ -3065,6 +3085,7 @@ Your browser does not support this feature. + Vaš preglednik ne podržava ovu značajku. src/app/components/block-overview-graph/block-overview-graph.component.html 21 @@ -3128,6 +3149,7 @@ Effective fee rate + Efektivna stopa naknade src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 52 @@ -3145,6 +3167,7 @@ Weight + Težina src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 63 @@ -3162,6 +3185,7 @@ Audit status + Status audita src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 67 @@ -3170,6 +3194,7 @@ Removed + Uklonjeno src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 71 @@ -3183,6 +3208,7 @@ Marginal fee rate + Granična stopa naknade src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 72 @@ -3195,6 +3221,7 @@ High sigop count + Veliki broj sigopa src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 73 @@ -3203,6 +3230,7 @@ Recently broadcasted + Nedavno emitirano src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 74 @@ -3211,6 +3239,7 @@ Recently CPFP'd + Nedavno CPFP src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 75 @@ -3219,6 +3248,7 @@ Added + Dodano src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 76 @@ -3232,6 +3262,7 @@ Prioritized + Prioritizirano src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 77 @@ -3245,6 +3276,7 @@ Conflict + Konflikt src/app/components/block-overview-tooltip/block-overview-tooltip.component.html 79 @@ -3258,6 +3290,7 @@ Block Rewards + Nagrade bloka src/app/components/block-rewards-graph/block-rewards-graph.component.html 7 @@ -3274,6 +3307,7 @@ See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees. + Pogledajte nagrade Bitcoin blokova u BTC-u i USD-u vizualizirane tijekom vremena. Nagrade za blok su ukupna sredstva koja rudari zarade od blok subsidy-a i naknada. src/app/components/block-rewards-graph/block-rewards-graph.component.ts 68 @@ -3281,6 +3315,7 @@ Block Sizes and Weights + Veličine i težine blokova src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html 5 @@ -3297,6 +3332,7 @@ See Bitcoin block sizes (MB) and block weights (weight units) visualized over time. + Pogledajte veličine Bitcoin blokova (MB) i težine blokova (jedinice težine) vizualizirane tijekom vremena. src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 65 @@ -3304,6 +3340,7 @@ Size + Veličina src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 187 @@ -3355,6 +3392,7 @@ Weight + Težina src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 195 @@ -3390,6 +3428,7 @@ Size per weight + Veličina po težini src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 203 @@ -3401,6 +3440,7 @@ Block : + Blok : src/app/components/block-view/block-view.component.ts 110 @@ -3416,6 +3456,7 @@ See size, weight, fee range, included transactions, and more for Liquid block (). + Pogledaj veličinu, težinu, raspon naknada, uključene transakcije i više za Liquid blok (). src/app/components/block-view/block-view.component.ts 112 @@ -3431,6 +3472,7 @@ See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin block (). + Pogledaj veličinu, težinu, raspon naknada, uključene transakcije, audit (očekivano v stvarno) i više za Bitcoin blok ( ). src/app/components/block-view/block-view.component.ts 114 @@ -3446,6 +3488,7 @@ Genesis + Genesis src/app/components/block/block-preview.component.html 10 @@ -3457,6 +3500,7 @@ Timestamp + Timestamp src/app/components/block/block-preview.component.html 26 @@ -3489,6 +3533,7 @@ Median fee + Srednja naknada src/app/components/block/block-preview.component.html 36 @@ -3505,6 +3550,7 @@ Total fees + Ukupne naknade src/app/components/block/block-preview.component.html 41 @@ -3538,6 +3584,7 @@ Miner + Rudar src/app/components/block/block-preview.component.html 53 @@ -3554,6 +3601,7 @@ transaction + transakcija src/app/components/block/block-transactions.component.html 4 @@ -3574,6 +3622,7 @@ transactions + transakcije src/app/components/block/block-transactions.component.html 5 @@ -3594,6 +3643,7 @@ Error loading data. + Pogreška pri učitavanju podataka. src/app/components/block/block-transactions.component.html 16 @@ -3614,6 +3664,7 @@ This block does not belong to the main chain, it has been replaced by: + Ovaj blok ne pripada glavnom lancu, zamijenjen je s: src/app/components/block/block.component.html 5 @@ -3623,6 +3674,7 @@ Previous Block + Prethodni blok src/app/components/block/block.component.html 19 @@ -3631,6 +3683,7 @@ Stale + Ustajao src/app/components/block/block.component.html 30 @@ -3640,6 +3693,7 @@ Hash + Haš src/app/components/block/block.component.html 44 @@ -3648,6 +3702,7 @@ Unknown + Nepoznato src/app/components/block/block.component.html 73 @@ -3704,6 +3759,7 @@ Fee span + Raspon naknada src/app/components/block/block.component.html 132 @@ -3716,6 +3772,7 @@ Based on average native segwit transaction of 140 vBytes + Na temelju prosječne native segwit transakcije od 140 vByte src/app/components/block/block.component.html 140 @@ -3744,6 +3801,7 @@ Subsidy + fees + Subsidy + naknade src/app/components/block/block.component.html 162 @@ -3757,6 +3815,7 @@ Expected + Očekivano src/app/components/block/block.component.html 225 @@ -3765,6 +3824,7 @@ Actual + Stvarno src/app/components/block/block.component.html 227 @@ -3773,6 +3833,7 @@ Expected Block + Očekivani blok src/app/components/block/block.component.html 231 @@ -3781,6 +3842,7 @@ Actual Block + Stvarni blok src/app/components/block/block.component.html 246 @@ -3789,6 +3851,7 @@ Version + Verzija src/app/components/block/block.component.html 273 @@ -3801,6 +3864,7 @@ Taproot + Taproot src/app/components/block/block.component.html 274 @@ -3830,6 +3894,7 @@ Bits + Bitovi src/app/components/block/block.component.html 277 @@ -3838,6 +3903,7 @@ Merkle root + Merkle root src/app/components/block/block.component.html 281 @@ -3846,6 +3912,7 @@ Difficulty + Težina src/app/components/block/block.component.html 292 @@ -3874,6 +3941,7 @@ Nonce + Nonce src/app/components/block/block.component.html 296 @@ -3882,6 +3950,7 @@ Block Header Hex + Hex zaglavlja bloka src/app/components/block/block.component.html 300 @@ -3890,6 +3959,7 @@ Audit + Audit src/app/components/block/block.component.html 318 @@ -3949,6 +4019,7 @@ Error loading block data. + Pogreška pri učitavanju podataka bloka. src/app/components/block/block.component.html 367 @@ -3957,6 +4028,7 @@ Why is this block empty? + Zašto je ovaj blok prazan? src/app/components/block/block.component.html 381 @@ -3965,6 +4037,7 @@ Acceleration fees paid out-of-band + Naknade za ubrzanje plaćene izvan pojasa src/app/components/block/block.component.html 413 @@ -3973,6 +4046,7 @@ Blocks + Blokovi src/app/components/blocks-list/blocks-list.component.html 4 @@ -3997,6 +4071,7 @@ Height + Visina src/app/components/blocks-list/blocks-list.component.html 12 @@ -4025,6 +4100,7 @@ Reward + Nagrada src/app/components/blocks-list/blocks-list.component.html 19 @@ -4057,6 +4133,7 @@ Fees + Naknade src/app/components/blocks-list/blocks-list.component.html 20 @@ -4073,6 +4150,7 @@ TXs + TX src/app/components/blocks-list/blocks-list.component.html 23 @@ -4109,6 +4187,7 @@ See the most recent Liquid blocks along with basic stats such as block height, block size, and more. + Pogledajte najnovije Liquid blokove zajedno s osnovnim statistikama kao što su visina bloka, veličina bloka i više. src/app/components/blocks-list/blocks-list.component.ts 71 @@ -4116,6 +4195,7 @@ See the most recent Bitcoin blocks along with basic stats such as block height, block reward, block size, and more. + Pogledajte najnovije Bitcoin blokove zajedno s osnovnim statistikama kao što su visina bloka, nagrada za blok, veličina bloka i više. src/app/components/blocks-list/blocks-list.component.ts 73 @@ -4123,6 +4203,7 @@ Calculator + Kalkulator src/app/components/calculator/calculator.component.html 3 @@ -4135,6 +4216,7 @@ Copied! + Kopirano! src/app/components/clipboard/clipboard.component.ts 19 @@ -4142,6 +4224,7 @@ Price + Cijena src/app/components/clock/clock.component.html 41 @@ -4149,6 +4232,7 @@ High Priority + Visoki prioritet src/app/components/clock/clock.component.html 47 @@ -4165,6 +4249,7 @@ Memory Usage + Upotreba memorije src/app/components/clock/clock.component.html 65 @@ -4182,6 +4267,7 @@ Unconfirmed + Nepotvrđeno src/app/components/clock/clock.component.html 69 @@ -4203,6 +4289,7 @@ Transaction Fees + Transakcijske naknade src/app/components/custom-dashboard/custom-dashboard.component.html 8 @@ -4215,6 +4302,7 @@ Incoming Transactions + Dolazne transakcije src/app/components/custom-dashboard/custom-dashboard.component.html 55 @@ -4231,6 +4319,7 @@ Minimum fee + Minimalna naknada src/app/components/custom-dashboard/custom-dashboard.component.html 71 @@ -4244,6 +4333,7 @@ Purging + Purging src/app/components/custom-dashboard/custom-dashboard.component.html 72 @@ -4257,6 +4347,7 @@ Recent Replacements + Nedavne zamjene src/app/components/custom-dashboard/custom-dashboard.component.html 100 @@ -4269,6 +4360,7 @@ Previous fee + Prethodna naknada src/app/components/custom-dashboard/custom-dashboard.component.html 107 @@ -4281,6 +4373,7 @@ New fee + Nova naknada src/app/components/custom-dashboard/custom-dashboard.component.html 108 @@ -4293,6 +4386,7 @@ Full RBF + Full RBF src/app/components/custom-dashboard/custom-dashboard.component.html 122 @@ -4313,6 +4407,7 @@ RBF + RBF src/app/components/custom-dashboard/custom-dashboard.component.html 123 @@ -4342,6 +4437,7 @@ Recent Blocks + Nedavni blokovi src/app/components/custom-dashboard/custom-dashboard.component.html 147 @@ -4362,6 +4458,7 @@ Recent Transactions + Nedavne transakcije src/app/components/custom-dashboard/custom-dashboard.component.html 190 @@ -4374,6 +4471,7 @@ Treasury + Riznica src/app/components/custom-dashboard/custom-dashboard.component.html 228 @@ -4382,6 +4480,7 @@ Treasury Transactions + Rizničke transakcije src/app/components/custom-dashboard/custom-dashboard.component.html 251 @@ -4390,6 +4489,7 @@ X Timeline + X Timeline src/app/components/custom-dashboard/custom-dashboard.component.html 265 @@ -4398,6 +4498,7 @@ Consolidation + Konsolidacija src/app/components/custom-dashboard/custom-dashboard.component.ts 72 @@ -4413,6 +4514,7 @@ Coinjoin + Coinjoin src/app/components/custom-dashboard/custom-dashboard.component.ts 73 @@ -4428,6 +4530,7 @@ Data + Podaci src/app/components/custom-dashboard/custom-dashboard.component.ts 74 @@ -4443,6 +4546,7 @@ Adjusted + Prilagođeno src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html 6 @@ -4451,6 +4555,7 @@ Change + Promijeniti src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html 8 @@ -4459,6 +4564,7 @@ Difficulty Adjustment + Prilagodba težine src/app/components/difficulty-mining/difficulty-mining.component.html 1 @@ -4475,6 +4581,7 @@ Remaining + Preostalo src/app/components/difficulty-mining/difficulty-mining.component.html 7 @@ -4487,6 +4594,7 @@ blocks + blokovi src/app/components/difficulty-mining/difficulty-mining.component.html 10,11 @@ -4511,6 +4619,7 @@ block + blok src/app/components/difficulty-mining/difficulty-mining.component.html 11,12 @@ -4527,6 +4636,7 @@ Estimate + Procjena src/app/components/difficulty-mining/difficulty-mining.component.html 16 @@ -4539,6 +4649,7 @@ Previous + Prethodno src/app/components/difficulty-mining/difficulty-mining.component.html 28 @@ -4551,6 +4662,7 @@ Current Period + Tekuće razdoblje src/app/components/difficulty-mining/difficulty-mining.component.html 40 @@ -4559,6 +4671,7 @@ Next Halving + Sljedeće prepolovljenje src/app/components/difficulty-mining/difficulty-mining.component.html 47 @@ -4571,6 +4684,7 @@ blocks expected + blokova očekivano src/app/components/difficulty/difficulty-tooltip.component.html 50 @@ -4579,6 +4693,7 @@ block expected + blok očekivan src/app/components/difficulty/difficulty-tooltip.component.html 51 @@ -4587,6 +4702,7 @@ blocks mined + blokova izrudareno src/app/components/difficulty/difficulty-tooltip.component.html 52 @@ -4595,6 +4711,7 @@ block mined + blok izrudaren src/app/components/difficulty/difficulty-tooltip.component.html 53 @@ -4603,6 +4720,7 @@ blocks remaining + blokova preostalo src/app/components/difficulty/difficulty-tooltip.component.html 54 @@ -4611,6 +4729,7 @@ block remaining + preostali blok src/app/components/difficulty/difficulty-tooltip.component.html 55 @@ -4619,6 +4738,7 @@ blocks ahead + blokova ispred src/app/components/difficulty/difficulty-tooltip.component.html 56 @@ -4627,6 +4747,7 @@ block ahead + blok ispred src/app/components/difficulty/difficulty-tooltip.component.html 57 @@ -4635,6 +4756,7 @@ blocks behind + blokova iza src/app/components/difficulty/difficulty-tooltip.component.html 58 @@ -4643,6 +4765,7 @@ block behind + blok iza src/app/components/difficulty/difficulty-tooltip.component.html 59 @@ -4651,6 +4774,7 @@ Halving Countdown + Odbrojavanje do prepolovljenja src/app/components/difficulty/difficulty.component.html 2 @@ -4659,6 +4783,7 @@ difficulty + težina src/app/components/difficulty/difficulty.component.html 7 @@ -4667,6 +4792,7 @@ halving + prepolovljenje src/app/components/difficulty/difficulty.component.html 10 @@ -4675,6 +4801,7 @@ Average block time + Prosječno vrijeme bloka src/app/components/difficulty/difficulty.component.html 50 @@ -4683,6 +4810,7 @@ New subsidy + Nova subvencija src/app/components/difficulty/difficulty.component.html 103 @@ -4691,6 +4819,7 @@ Blocks remaining + Blokova preostalo src/app/components/difficulty/difficulty.component.html 111 @@ -4699,6 +4828,7 @@ Block remaining + Preostali blok src/app/components/difficulty/difficulty.component.html 112 @@ -4707,6 +4837,7 @@ Testnet4 Faucet + Testnet4 Faucet src/app/components/faucet/faucet.component.html 4 @@ -4715,6 +4846,7 @@ Amount (sats) + Iznos (sat) src/app/components/faucet/faucet.component.html 51 @@ -4723,6 +4855,7 @@ Request Testnet4 Coins + Zatraži Testnet4 coinove src/app/components/faucet/faucet.component.html 70 @@ -4731,6 +4864,7 @@ Either 2x the minimum, or the Low Priority rate (whichever is lower) + Ili 2x minimalna ili stopa niskog prioriteta (što god je niže) src/app/components/fees-box/fees-box.component.html 4 @@ -4739,6 +4873,7 @@ No Priority + Bez prioriteta src/app/components/fees-box/fees-box.component.html 4 @@ -4751,6 +4886,7 @@ Usually places your transaction in between the second and third mempool blocks + Obično smješta vašu transakciju između drugog i trećeg bloka mempoola src/app/components/fees-box/fees-box.component.html 8 @@ -4759,6 +4895,7 @@ Low Priority + Nizak prioritet src/app/components/fees-box/fees-box.component.html 8 @@ -4771,6 +4908,7 @@ Usually places your transaction in between the first and second mempool blocks + Obično smješta vašu transakciju između prvog i drugog bloka mempoola src/app/components/fees-box/fees-box.component.html 9 @@ -4779,6 +4917,7 @@ Medium Priority + Srednji prioritet src/app/components/fees-box/fees-box.component.html 9 @@ -4791,6 +4930,7 @@ Places your transaction in the first mempool block + Smješta vašu transakciju u prvi mempool blok src/app/components/fees-box/fees-box.component.html 10 @@ -4799,6 +4939,7 @@ Backend is synchronizing + Pozadina se sinkronizira src/app/components/footer/footer.component.html 8 @@ -4807,6 +4948,7 @@ vB/s + vB/s src/app/components/footer/footer.component.html 13 @@ -4816,6 +4958,7 @@ WU/s + WU/s src/app/components/footer/footer.component.html 14 @@ -4825,6 +4968,7 @@ Mempool size + Veličina Mempoola src/app/components/footer/footer.component.html 24 @@ -4834,6 +4978,7 @@ Mining + Rudarenje src/app/components/graphs/graphs.component.html 7 @@ -4842,6 +4987,7 @@ Pools Ranking + Poredak pool-ova src/app/components/graphs/graphs.component.html 10 @@ -4854,6 +5000,7 @@ Pools Dominance + Dominacija pool-ova src/app/components/graphs/graphs.component.html 12 @@ -4866,6 +5013,7 @@ Hashrate & Difficulty + Hashrate i težina src/app/components/graphs/graphs.component.html 14 @@ -4882,6 +5030,7 @@ Lightning + Lightning src/app/components/graphs/graphs.component.html 31 @@ -4890,6 +5039,7 @@ Lightning Nodes Per Network + Lightning nodova po mreži src/app/components/graphs/graphs.component.html 34 @@ -4910,6 +5060,7 @@ Lightning Network Capacity + Kapacitet Lightning mreže src/app/components/graphs/graphs.component.html 36 @@ -4930,6 +5081,7 @@ Lightning Nodes Per ISP + Lightning nodova po ISP-u src/app/components/graphs/graphs.component.html 38 @@ -4942,6 +5094,7 @@ Lightning Nodes Per Country + Lightning nodovi po zemlji src/app/components/graphs/graphs.component.html 40 @@ -4958,6 +5111,7 @@ Lightning Nodes World Map + Karta svijeta lightning nodova src/app/components/graphs/graphs.component.html 42 @@ -4974,6 +5128,7 @@ Lightning Nodes Channels World Map + Karta svijeta kanala lightning nodova src/app/components/graphs/graphs.component.html 44 @@ -4986,6 +5141,7 @@ Hashrate + Hashrate src/app/components/hashrate-chart/hashrate-chart.component.html 8 @@ -5022,6 +5178,7 @@ See hashrate and difficulty for the Bitcoin network visualized over time. + Pogledajte hashrate i težinu za Bitcoin mrežu vizualiziranu tijekom vremena. src/app/components/hashrate-chart/hashrate-chart.component.ts 76 @@ -5029,6 +5186,7 @@ Hashrate (MA) + Hashrate (MA) src/app/components/hashrate-chart/hashrate-chart.component.ts 318 @@ -5040,6 +5198,7 @@ Pools Historical Dominance + Povijesna dominacija pool-ova src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts 74 @@ -5047,6 +5206,7 @@ See Bitcoin mining pool dominance visualized over time: see how top mining pools' share of total hashrate has fluctuated over time. + Pogledajte vizualizaciju dominacije pool-ova za rudarenje Bitcoina tijekom vremena: pogledajte kako je udio najvećih pool-ova za rudarenje u ukupnom hashrateu fluktuirao tijekom vremena. src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts 75 @@ -5054,6 +5214,7 @@ Indexing network hashrate + Indeksiranje hashrate-a mreže src/app/components/indexing-progress/indexing-progress.component.html 2 @@ -5061,6 +5222,7 @@ Indexing pools hashrate + Indeksiranje hashrate-a pool-ova src/app/components/indexing-progress/indexing-progress.component.html 3 @@ -5068,6 +5230,7 @@ Offline + Offline src/app/components/liquid-master-page/liquid-master-page.component.html 41 @@ -5084,6 +5247,7 @@ Reconnecting... + Ponovno povezivanje... src/app/components/liquid-master-page/liquid-master-page.component.html 42 @@ -5100,6 +5264,7 @@ Layer 2 Networks + Mreže sloja 2 src/app/components/liquid-master-page/liquid-master-page.component.html 56 @@ -5108,6 +5273,7 @@ Dashboard + Nadzorna ploča src/app/components/liquid-master-page/liquid-master-page.component.html 65 @@ -5120,6 +5286,7 @@ Graphs + Grafikoni src/app/components/liquid-master-page/liquid-master-page.component.html 71 @@ -5136,6 +5303,7 @@ Documentation + Dokumentacija src/app/components/liquid-master-page/liquid-master-page.component.html 82 @@ -5152,6 +5320,7 @@ Non-Dust Expired + Non-Dust Isteklo src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 3 @@ -5160,6 +5329,7 @@ Total amount of BTC held in non-dust Federation UTXOs that have expired timelocks + Ukupan iznos BTC-a koji se čuva u non-dust UTXO-ima Federacije kojima je isteklo vremensko zaključavanje src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 5 @@ -5168,6 +5338,7 @@ UTXOs + UTXO-ovi src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 6 @@ -5184,6 +5355,7 @@ Total Expired + Ukupno isteklo src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 12 @@ -5192,6 +5364,7 @@ Total amount of BTC held in Federation UTXOs that have expired timelocks + Ukupan iznos BTC-a koji se čuva u federacijskim UTXO-ima kojima je isteklo vremensko zaključavanje src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 15 @@ -5200,6 +5373,7 @@ Liquid Federation Wallet + Liquid Federacija novčanik src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.html 5 @@ -5220,6 +5394,7 @@ addresses + adrese src/app/components/liquid-reserves-audit/federation-addresses-stats/federation-addresses-stats.component.html 8 @@ -5228,6 +5403,7 @@ Output + Output src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 8 @@ -5244,6 +5420,7 @@ Related Peg-In + Povezani Peg-In src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 11 @@ -5252,6 +5429,7 @@ Expires in + Istječe za src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 13 @@ -5260,6 +5438,7 @@ Expired since + Isteklo od src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 14 @@ -5268,6 +5447,7 @@ Dust + Dust src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 15 @@ -5276,6 +5456,7 @@ Change output + Output ostatka src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 55 @@ -5284,6 +5465,7 @@ blocks + blokovi src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 63 @@ -5292,6 +5474,7 @@ Timelock-Expired UTXOs + UTXO-i s istekom vremenskog zaključavanja src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.html 12 @@ -5300,6 +5483,7 @@ Addresses + Adrese src/app/components/liquid-reserves-audit/federation-wallet/federation-wallet.component.html 15 @@ -5324,6 +5508,7 @@ Recent Peg-In / Out's + Nedavni Peg-In / Out-ovi src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html 4 @@ -5344,6 +5529,7 @@ Fund / Redemption Tx + Fund/ Otkup Tx src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html 15 @@ -5352,6 +5538,7 @@ BTC Address + BTC adresa src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html 16 @@ -5360,6 +5547,7 @@ Peg out in progress... + Peg out u tijeku... src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html 70 @@ -5368,6 +5556,7 @@ 24h Peg-In Volume + 24h Peg-In Volumen src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html 12 @@ -5376,6 +5565,7 @@ Peg-Ins + Peg-In-ovi src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html 13 @@ -5384,6 +5574,7 @@ 24h Peg-Out Volume + 24-satni volumen Peg-Out src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html 18 @@ -5392,6 +5583,7 @@ Peg-Outs + Peg-Out-ovi src/app/components/liquid-reserves-audit/recent-pegs-stats/recent-pegs-stats.component.html 19 @@ -5400,6 +5592,7 @@ Unpeg + Otpeg src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 3 @@ -5408,6 +5601,7 @@ Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply + Koliko puta BTC posjed Federacije padne ispod 95% ukupne ponude L-BTC-a src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 6 @@ -5416,6 +5610,7 @@ Unpeg Event + Unpeg događaj src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 7 @@ -5424,6 +5619,7 @@ Avg Peg Ratio + Prosječni omjer pega src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 14 @@ -5432,6 +5628,7 @@ Emergency Keys + Ključevi za hitnoće src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 28 @@ -5440,6 +5637,7 @@ usage + korištenje src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 31 @@ -5448,6 +5646,7 @@ Assets vs Liabilities + Imovina vs obveze src/app/components/liquid-reserves-audit/reserves-ratio/reserves-ratio.component.ts 163 @@ -5455,6 +5654,7 @@ L-BTC in circulation + L-BTC u opticaju src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html 3 @@ -5463,6 +5663,7 @@ As of block + Od bloka src/app/components/liquid-reserves-audit/reserves-supply-stats/reserves-supply-stats.component.html 7 @@ -5475,6 +5676,7 @@ Mining Dashboard + Nadzor rudarenja src/app/components/master-page/master-page.component.html 92 @@ -5491,6 +5693,7 @@ Lightning Explorer + Lightning explorer src/app/components/master-page/master-page.component.html 95 @@ -5507,6 +5710,7 @@ Faucet + Faucet src/app/components/master-page/master-page.component.html 105 @@ -5515,6 +5719,7 @@ See stats for transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions. + Pogledajte statistiku za transakcije u mempoolu: raspon naknada, ukupna veličina i više. Blokovi Mempoola ažuriraju se u stvarnom vremenu kako mreža prima nove transakcije. src/app/components/mempool-block/mempool-block.component.ts 62 @@ -5522,6 +5727,7 @@ Stack of mempool blocks + Hrpa od mempool blokova src/app/components/mempool-block/mempool-block.component.ts 89 @@ -5529,6 +5735,7 @@ Mempool block + Mempool blok src/app/components/mempool-block/mempool-block.component.ts 91 @@ -5536,6 +5743,7 @@ Count + Račun src/app/components/mempool-graph/mempool-graph.component.ts 329 @@ -5547,6 +5755,7 @@ Range + Raspon src/app/components/mempool-graph/mempool-graph.component.ts 330 @@ -5554,6 +5763,7 @@ Sum + Zbroj src/app/components/mempool-graph/mempool-graph.component.ts 332 @@ -5561,6 +5771,7 @@ Sign In + Prijavi se src/app/components/menu/menu.component.html 21 @@ -5577,6 +5788,7 @@ Reward stats + Statistika nagrada src/app/components/mining-dashboard/mining-dashboard.component.html 9 @@ -5585,6 +5797,7 @@ (144 blocks) + (144 bloka) src/app/components/mining-dashboard/mining-dashboard.component.html 10 @@ -5593,6 +5806,7 @@ Adjustments + Prilagodbe src/app/components/mining-dashboard/mining-dashboard.component.html 70 @@ -5601,6 +5815,7 @@ Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more. + Dobijte statistiku rudarenja Bitcoina u stvarnom vremenu kao što je hashrate, prilagodba težine, blok nagrade, dominacija pool-ova i više. src/app/components/mining-dashboard/mining-dashboard.component.ts 30 @@ -5608,6 +5823,7 @@ Pools luck (1 week) + Uspjeh pool-ova (1 tjedan) src/app/components/pool-ranking/pool-ranking.component.html 9 @@ -5616,6 +5832,7 @@ Pools Luck + Uspjeh pool-ova src/app/components/pool-ranking/pool-ranking.component.html 9 diff --git a/frontend/src/locale/messages.ko.xlf b/frontend/src/locale/messages.ko.xlf index 7e80a2387..47c1eac43 100644 --- a/frontend/src/locale/messages.ko.xlf +++ b/frontend/src/locale/messages.ko.xlf @@ -457,6 +457,7 @@ Plus unconfirmed ancestor(s) + 컨펌되지 않은 조상(들) src/app/components/accelerate-checkout/accelerate-checkout.component.html 41 @@ -491,6 +492,7 @@ Size in vbytes of this transaction (including unconfirmed ancestors) + 트랜잭션 크기 (확인되지 않은 조상 포함) src/app/components/accelerate-checkout/accelerate-checkout.component.html 51 @@ -499,6 +501,7 @@ In-band fees + 대역 내 수수료 src/app/components/accelerate-checkout/accelerate-checkout.component.html 55 @@ -624,6 +627,7 @@ Fees already paid by this transaction (including unconfirmed ancestors) + 이 트랜잭션이 이미 지불한 수수료 (확인되지 않은 조상 포함) src/app/components/accelerate-checkout/accelerate-checkout.component.html 62 @@ -632,6 +636,7 @@ How much faster? + 얼마나 더 빠르게 원하시나요? src/app/components/accelerate-checkout/accelerate-checkout.component.html 71 @@ -640,6 +645,7 @@ This will reduce your expected waiting time until the first confirmation to + 첫 번째 컨펌까지 예상 대기 시간이 로 단축됩니다 src/app/components/accelerate-checkout/accelerate-checkout.component.html 76,77 @@ -657,6 +663,7 @@ Next block market rate + 다음 블록 시장 수수료율 src/app/components/accelerate-checkout/accelerate-checkout.component.html 109 @@ -687,6 +694,7 @@ Estimated extra fee required + 예측된 추가발생 수수료 src/app/components/accelerate-checkout/accelerate-checkout.component.html 117 @@ -695,6 +703,7 @@ Target rate + 목표율 src/app/components/accelerate-checkout/accelerate-checkout.component.html 131 @@ -703,6 +712,7 @@ Extra fee required + 필요한 추가 수수료 src/app/components/accelerate-checkout/accelerate-checkout.component.html 139 @@ -711,6 +721,7 @@ Mempool Accelerator™ fees + 멤풀 엑셀러레이터 수수료 src/app/components/accelerate-checkout/accelerate-checkout.component.html 153 @@ -719,6 +730,7 @@ Accelerator Service Fee + 엑셀러레이터 서비스 수수료 src/app/components/accelerate-checkout/accelerate-checkout.component.html 157 @@ -727,6 +739,7 @@ Transaction Size Surcharge + 트랜잭션 사이즈에 의한 추가 요금 src/app/components/accelerate-checkout/accelerate-checkout.component.html 169 @@ -735,6 +748,7 @@ Estimated acceleration cost + 예상 가속 비용 src/app/components/accelerate-checkout/accelerate-checkout.component.html 185 @@ -743,6 +757,7 @@ Maximum acceleration cost + 최대 가속 비용 src/app/components/accelerate-checkout/accelerate-checkout.component.html 204 @@ -760,6 +775,7 @@ Available balance + 잔액 src/app/components/accelerate-checkout/accelerate-checkout.component.html 226 @@ -785,6 +801,7 @@ Accelerate your Bitcoin transaction? + 비트코인 트랜잭션 가속하기 src/app/components/accelerate-checkout/accelerate-checkout.component.html 273 @@ -802,6 +819,7 @@ Confirmation expected + 컨펌이 예상됩니다 src/app/components/accelerate-checkout/accelerate-checkout.component.html 287 @@ -1587,6 +1605,7 @@ Total vSize + 총 vSize src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html 20 From 5178ae43f621b188b750211c9e9c6d1d20e7953b Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 12 Aug 2024 00:07:48 +0200 Subject: [PATCH 08/73] Add Croatian language --- frontend/src/app/app.constants.ts | 2 +- nginx.conf | 2 ++ production/nginx/http-language.conf | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index aaa53b8ba..cef630984 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -151,7 +151,7 @@ export const languages: Language[] = [ { code: 'fr', name: 'Français' }, // French // { code: 'gl', name: 'Galego' }, // Galician { code: 'ko', name: '한국어' }, // Korean -// { code: 'hr', name: 'Hrvatski' }, // Croatian + { code: 'hr', name: 'Hrvatski' }, // Croatian // { code: 'id', name: 'Bahasa Indonesia' },// Indonesian { code: 'hi', name: 'हिन्दी' }, // Hindi { code: 'ne', name: 'नेपाली' }, // Nepalese diff --git a/nginx.conf b/nginx.conf index abd7b1269..670764e20 100644 --- a/nginx.conf +++ b/nginx.conf @@ -108,6 +108,7 @@ http { ~*^hi hi; ~*^ne ne; ~*^lt lt; + ~*^hr hr; } map $cookie_lang $lang { @@ -145,6 +146,7 @@ http { ~*^hi hi; ~*^ne ne; ~*^lt lt; + ~*^hr hr; } server { diff --git a/production/nginx/http-language.conf b/production/nginx/http-language.conf index c03d776b0..14c26a741 100644 --- a/production/nginx/http-language.conf +++ b/production/nginx/http-language.conf @@ -32,6 +32,7 @@ map $http_accept_language $header_lang { ~*^vi vi; ~*^zh zh; ~*^lt lt; + ~*^hr hr; } map $cookie_lang $lang { default $header_lang; @@ -67,4 +68,5 @@ map $cookie_lang $lang { ~*^vi vi; ~*^zh zh; ~*^lt lt; + ~*^hr hr; } From 96bec279a91a4007743d61b4df9ae87454def64b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 12 Aug 2024 14:54:51 +0000 Subject: [PATCH 09/73] flow diagram zero-indexed inputs & outputs --- .../tx-bowtie-graph-tooltip.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html index 6aa7cf7c0..3dfb2059d 100644 --- a/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html +++ b/frontend/src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html @@ -43,7 +43,7 @@ Output Fee - #{{ line.index + 1 }} + #{{ line.index }} @@ -73,7 +73,7 @@

-

Output  #{{ line.vout + 1 }} +

Output  #{{ line.vout }} @@ -83,7 +83,7 @@

-

Input  #{{ line.vin + 1 }} +

Input  #{{ line.vin }} From db10ab9aae248c9af2b0c7e5082e2cd97c13fd5a Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 13 Aug 2024 10:28:42 +0200 Subject: [PATCH 10/73] pull from transifex --- frontend/src/locale/messages.hr.xlf | 377 +++++++++++++++++++++++++++- frontend/src/locale/messages.tr.xlf | 52 ++++ 2 files changed, 427 insertions(+), 2 deletions(-) diff --git a/frontend/src/locale/messages.hr.xlf b/frontend/src/locale/messages.hr.xlf index 5a37a7ff7..e0f2ceb89 100644 --- a/frontend/src/locale/messages.hr.xlf +++ b/frontend/src/locale/messages.hr.xlf @@ -5845,6 +5845,7 @@ The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes. + Ukupan uspjeh svih rudarskih pool-ova tijekom prošlog tjedna. Uspjeh veći od 100% znači da je prosječno vrijeme bloka za trenutnu epohu manje od 10 minuta. src/app/components/pool-ranking/pool-ranking.component.html 11 @@ -5853,6 +5854,7 @@ Pools count (1w) + Broj pool-ova (1w) src/app/components/pool-ranking/pool-ranking.component.html 17 @@ -5861,6 +5863,7 @@ Pools Count + Broj pool-ova src/app/components/pool-ranking/pool-ranking.component.html 17 @@ -5873,6 +5876,7 @@ How many unique pools found at least one block over the past week. + Koliko je pojedinih pool-ova pronašlo barem jedan blok tijekom prošlog tjedna. src/app/components/pool-ranking/pool-ranking.component.html 19 @@ -5881,6 +5885,7 @@ Blocks (1w) + Blokova (1w) src/app/components/pool-ranking/pool-ranking.component.html 25 @@ -5897,6 +5902,7 @@ The number of blocks found over the past week. + Broj blokova pronađenih tijekom prošlog tjedna. src/app/components/pool-ranking/pool-ranking.component.html 27 @@ -5905,6 +5911,7 @@ Rank + Rang src/app/components/pool-ranking/pool-ranking.component.html 90 @@ -5921,6 +5928,7 @@ Avg Health + Prosj. zdravlje src/app/components/pool-ranking/pool-ranking.component.html 96 @@ -5945,6 +5953,7 @@ Avg Block Fees + Prosječne naknade bloka src/app/components/pool-ranking/pool-ranking.component.html 97 @@ -5965,6 +5974,7 @@ Empty Blocks + Prazni blokovi src/app/components/pool-ranking/pool-ranking.component.html 98 @@ -5973,6 +5983,7 @@ All miners + Svi rudari src/app/components/pool-ranking/pool-ranking.component.html 138 @@ -5981,6 +5992,7 @@ Mining Pools + Rudarski pool-ovi src/app/components/pool-ranking/pool-ranking.component.ts 59 @@ -5988,6 +6000,7 @@ See the top Bitcoin mining pools ranked by number of blocks mined, over your desired timeframe. + Pogledajte najbolje rudarske pool-ove Bitcoina poredane prema broju iskopanih blokova u željenom vremenskom okviru. src/app/components/pool-ranking/pool-ranking.component.ts 60 @@ -5995,6 +6008,7 @@ blocks + blokova src/app/components/pool-ranking/pool-ranking.component.ts 167 @@ -6014,6 +6028,7 @@ Other () + Ostali () src/app/components/pool-ranking/pool-ranking.component.ts 186 @@ -6045,6 +6060,7 @@ mining pool + rudarski pool src/app/components/pool/pool-preview.component.html 3 @@ -6053,6 +6069,7 @@ Tags + Oznake src/app/components/pool/pool-preview.component.html 18 @@ -6077,6 +6094,7 @@ See mining pool stats for : most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more. + Pogledajte statistiku rudarskih pool-ova za : najnovije iskopane blokove, hashrate tijekom vremena, ukupnu nagradu za blok do danas, poznate coinbase adrese i više. src/app/components/pool/pool-preview.component.ts 86 @@ -6088,6 +6106,7 @@ Show all + Prikaži sve src/app/components/pool/pool.component.html 53 @@ -6120,6 +6139,7 @@ Hide + Sakrij src/app/components/pool/pool.component.html 55 @@ -6128,6 +6148,7 @@ Hashrate (24h) + Hashrate (24h) src/app/components/pool/pool.component.html 95 @@ -6144,6 +6165,7 @@ Blocks (24h) + Blokovi (24h) src/app/components/pool/pool.component.html 120 @@ -6160,6 +6182,7 @@ 1w + 1w src/app/components/pool/pool.component.html 121 @@ -6176,6 +6199,7 @@ Out-of-band Fees (1w) + Izvanpojasne naknade (1w) src/app/components/pool/pool.component.html 143 @@ -6184,6 +6208,7 @@ 1m + 1m src/app/components/pool/pool.component.html 144 @@ -6192,6 +6217,7 @@ Coinbase tag + Coinbase oznaka src/app/components/pool/pool.component.html 184 @@ -6204,6 +6230,7 @@ Error loading pool data. + Pogreška pri učitavanju podataka pool-a. src/app/components/pool/pool.component.html 467 @@ -6212,6 +6239,7 @@ Not enough data yet + Još nema dovoljno podataka src/app/components/pool/pool.component.ts 142 @@ -6219,6 +6247,7 @@ Pool Dominance + Pool dominacija src/app/components/pool/pool.component.ts 222 @@ -6231,6 +6260,7 @@ Broadcast Transaction + Emitiraj transakciju src/app/components/push-transaction/push-transaction.component.html 2 @@ -6248,6 +6278,7 @@ Transaction hex + Hex transakcije src/app/components/push-transaction/push-transaction.component.html 6 @@ -6260,6 +6291,7 @@ Broadcast Transaction + Emitiraj transakciju src/app/components/push-transaction/push-transaction.component.ts 38 @@ -6267,6 +6299,7 @@ Broadcast a transaction to the network using the transaction's hash. + Emitiraj transakciju na mrežu koristeći hash transakcije. src/app/components/push-transaction/push-transaction.component.ts 39 @@ -6274,6 +6307,7 @@ RBF Replacements + RBF zamjene src/app/components/rbf-list/rbf-list.component.html 2 @@ -6286,6 +6320,7 @@ There are no replacements in the mempool yet! + Još nema zamjena u mempoolu! src/app/components/rbf-list/rbf-list.component.html 34 @@ -6294,6 +6329,7 @@ See the most recent RBF replacements on the Bitcoin network, updated in real-time. + Pogledaj najnovije zamjene RBF-a na Bitcoin mreži, ažurirane u stvarnom vremenu. src/app/components/rbf-list/rbf-list.component.ts 62 @@ -6301,6 +6337,7 @@ Show less + Prikaži manje src/app/components/rbf-timeline/rbf-timeline.component.html 61 @@ -6321,6 +6358,7 @@ remaining + preostalo src/app/components/rbf-timeline/rbf-timeline.component.html 86 @@ -6333,6 +6371,7 @@ Miners Reward + Nagrada za rudare src/app/components/reward-stats/reward-stats.component.html 5 @@ -6349,6 +6388,7 @@ Amount being paid to miners in the past 144 blocks + Iznos isplaćen rudarima u protekla 144 bloka src/app/components/reward-stats/reward-stats.component.html 6 @@ -6357,6 +6397,7 @@ Average fees per block in the past 144 blocks + Prosječne naknade po bloku u posljednja 144 bloka src/app/components/reward-stats/reward-stats.component.html 18 @@ -6365,6 +6406,7 @@ BTC/block + BTC/blok src/app/components/reward-stats/reward-stats.component.html 21 @@ -6374,6 +6416,7 @@ Avg Tx Fee + Prosječna naknada za transakciju src/app/components/reward-stats/reward-stats.component.html 30 @@ -6390,6 +6433,7 @@ Fee paid on average for each transaction in the past 144 blocks + Naknada plaćena u prosjeku za svaku transakciju u posljednja 144 bloka src/app/components/reward-stats/reward-stats.component.html 31 @@ -6398,6 +6442,7 @@ sats/tx + sat/tx src/app/components/reward-stats/reward-stats.component.html 33 @@ -6407,6 +6452,7 @@ Explore the full Bitcoin ecosystem + Istražite cijeli Bitcoin ekosustav src/app/components/search-form/search-form.component.html 4 @@ -6423,6 +6469,7 @@ Search + Pretraživanje src/app/components/search-form/search-form.component.html 9 @@ -6431,6 +6478,7 @@ Block Height + Visina bloka src/app/components/search-form/search-results/search-results.component.html 3 @@ -6439,6 +6487,7 @@ Transaction + Transakcija src/app/components/search-form/search-results/search-results.component.html 21 @@ -6447,6 +6496,7 @@ Address + Adresa src/app/components/search-form/search-results/search-results.component.html 27 @@ -6459,6 +6509,7 @@ Block + Blok src/app/components/search-form/search-results/search-results.component.html 33 @@ -6467,6 +6518,7 @@ Addresses + Adrese src/app/components/search-form/search-results/search-results.component.html 39 @@ -6475,6 +6527,7 @@ Mining Pools + Rudarski pool-ovi src/app/components/search-form/search-results/search-results.component.html 47 @@ -6483,6 +6536,7 @@ Lightning Nodes + Lightning nodovi src/app/components/search-form/search-results/search-results.component.html 56 @@ -6491,6 +6545,7 @@ Lightning Channels + Lightning kanali src/app/components/search-form/search-results/search-results.component.html 64 @@ -6499,6 +6554,7 @@ Other Network Address + Druga mrežna adresa src/app/components/search-form/search-results/search-results.component.html 72 @@ -6507,6 +6563,7 @@ Liquid Asset + Liquid asset src/app/components/search-form/search-results/search-results.component.html 86 @@ -6515,6 +6572,7 @@ Go to "" + Idi na &quot;&quot; src/app/components/search-form/search-results/search-results.component.html 93 @@ -6523,6 +6581,7 @@ Mempool by vBytes (sat/vByte) + Mempool po vBytes (sat/vByte) src/app/components/statistics/statistics.component.html 7 @@ -6531,6 +6590,7 @@ Clock (Mempool) + Sat (Mempool) src/app/components/statistics/statistics.component.html 17 @@ -6543,6 +6603,7 @@ TV view + TV pogled src/app/components/statistics/statistics.component.html 20 @@ -6555,6 +6616,7 @@ Filter + Filter src/app/components/statistics/statistics.component.html 68 @@ -6563,6 +6625,7 @@ Invert + Preokrenuti src/app/components/statistics/statistics.component.html 93 @@ -6571,6 +6634,7 @@ Transaction vBytes per second (vB/s) + Transakcija vBytes u sekundi (vB/s) src/app/components/statistics/statistics.component.html 113 @@ -6579,6 +6643,7 @@ Cap outliers + Cap odstupanja src/app/components/statistics/statistics.component.html 121 @@ -6587,6 +6652,7 @@ See mempool size (in MvB) and transactions per second (in vB/s) visualized over time. + Pogledajte veličinu mempoola (u MvB) i transakcije po sekundi (u vB/s) vizualizirane tijekom vremena. src/app/components/statistics/statistics.component.ts 66 @@ -6594,6 +6660,7 @@ See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV. + Pogledajte Bitcoin blokove i zagušenja mempoola u stvarnom vremenu u pojednostavljenom formatu savršenom za TV. src/app/components/television/television.component.ts 40 @@ -6601,6 +6668,7 @@ Test Transactions + Testne transakcije src/app/components/test-transactions/test-transactions.component.html 2 @@ -6618,6 +6686,7 @@ Raw hex + Sirovi hex src/app/components/test-transactions/test-transactions.component.html 5 @@ -6626,6 +6695,7 @@ Comma-separated list of raw transactions + Popis neobrađenih transakcija odvojenih zarezima src/app/components/test-transactions/test-transactions.component.html 7 @@ -6634,6 +6704,7 @@ Maximum fee rate (sat/vB) + Maksimalna stopa naknade (sat/vB) src/app/components/test-transactions/test-transactions.component.html 9 @@ -6642,6 +6713,7 @@ Allowed? + Dopušteno? src/app/components/test-transactions/test-transactions.component.html 23 @@ -6650,6 +6722,7 @@ Rejection reason + Razlog odbijanja src/app/components/test-transactions/test-transactions.component.html 26 @@ -6658,6 +6731,7 @@ Immediately + Odmah src/app/components/time/time.component.ts 107 @@ -6665,6 +6739,7 @@ Just now + Upravo sada src/app/components/time/time.component.ts 111 @@ -6680,6 +6755,7 @@ ago + Prije src/app/components/time/time.component.ts 165 @@ -6739,6 +6815,7 @@ In ~ + U ~ src/app/components/time/time.component.ts 188 @@ -6798,6 +6875,7 @@ within ~ + unutar ~ src/app/components/time/time.component.ts 211 @@ -6857,6 +6935,7 @@ After + Nakon src/app/components/time/time.component.ts 234 @@ -6916,6 +6995,7 @@ before + prije src/app/components/time/time.component.ts 257 @@ -6975,6 +7055,7 @@ Sent + Poslano src/app/components/tracker/tracker-bar.component.html 2 @@ -6983,6 +7064,7 @@ Soon + Uskoro src/app/components/tracker/tracker-bar.component.html 6 @@ -6991,7 +7073,7 @@ This transaction has been replaced by: - Ova transakcija je zamijenja od: + Ova transakcija je zamijenjena sa: src/app/components/tracker/tracker.component.html 48 @@ -7005,6 +7087,7 @@ ETA + ETA src/app/components/tracker/tracker.component.html 69 @@ -7018,6 +7101,7 @@ Not any time soon + Ne u skorije vrijeme src/app/components/tracker/tracker.component.html 74 @@ -7031,6 +7115,7 @@ Confirmed at + Potvrđeno u src/app/components/tracker/tracker.component.html 87 @@ -7039,6 +7124,7 @@ Block height + Visina bloka src/app/components/tracker/tracker.component.html 96 @@ -7047,6 +7133,7 @@ Your transaction has been accelerated + Vaša transakcija je ubrzana src/app/components/tracker/tracker.component.html 143 @@ -7055,6 +7142,7 @@ Waiting for your transaction to appear in the mempool + Čeka se da se vaša transakcija pojavi u mempoolu src/app/components/tracker/tracker.component.html 150 @@ -7063,6 +7151,7 @@ Your transaction is in the mempool, but it will not be confirmed for some time. + Vaša transakcija je u mempoolu, ali još neko vrijeme neće biti potvrđena. src/app/components/tracker/tracker.component.html 156 @@ -7071,6 +7160,7 @@ Your transaction is near the top of the mempool, and is expected to confirm soon. + Vaša je transakcija pri vrhu mempoola i očekuje se skora potvrda. src/app/components/tracker/tracker.component.html 162 @@ -7079,6 +7169,7 @@ Your transaction is expected to confirm in the next block + Očekuje se da će vaša transakcija biti potvrđena u sljedećem bloku src/app/components/tracker/tracker.component.html 168 @@ -7087,6 +7178,7 @@ Your transaction is confirmed! + Vaša transakcija je potvrđena! src/app/components/tracker/tracker.component.html 174 @@ -7095,6 +7187,7 @@ Your transaction has been replaced by a newer version! + Vaša je transakcija zamijenjena novijom verzijom! src/app/components/tracker/tracker.component.html 180 @@ -7103,6 +7196,7 @@ See more details + Pogledajte više detalja src/app/components/tracker/tracker.component.html 193 @@ -7111,6 +7205,7 @@ Transaction: + Transakcija: src/app/components/tracker/tracker.component.ts 409 @@ -7126,6 +7221,7 @@ Get real-time status, addresses, fees, script info, and more for transaction with txid . + Dobijte status u stvarnom vremenu, adrese, naknade, informacije o skripti i više za transakciju s txid . src/app/components/tracker/tracker.component.ts 413 @@ -7141,6 +7237,7 @@ Coinbase + Coinbase src/app/components/transaction/transaction-preview.component.html 43 @@ -7161,6 +7258,7 @@ Descendant + Potomak src/app/components/transaction/transaction.component.html 88 @@ -7174,6 +7272,7 @@ Ancestor + Predak src/app/components/transaction/transaction.component.html 112 @@ -7183,6 +7282,7 @@ Hide accelerator + Sakrij akcelerator src/app/components/transaction/transaction.component.html 133 @@ -7191,6 +7291,7 @@ RBF Timeline + RBF kroz vrijeme src/app/components/transaction/transaction.component.html 160 @@ -7200,6 +7301,7 @@ Acceleration Timeline + Ubrzanja kroz vrijeme src/app/components/transaction/transaction.component.html 169 @@ -7209,6 +7311,7 @@ Flow + Protok src/app/components/transaction/transaction.component.html 178 @@ -7222,6 +7325,7 @@ Hide diagram + Sakrij dijagram src/app/components/transaction/transaction.component.html 181 @@ -7230,6 +7334,7 @@ Show more + Prikaži više src/app/components/transaction/transaction.component.html 202 @@ -7246,7 +7351,7 @@ Inputs & Outputs - Ulazi & Izlazi + Inputi i outputi src/app/components/transaction/transaction.component.html 220 @@ -7260,6 +7365,7 @@ Show diagram + Prikaži dijagram src/app/components/transaction/transaction.component.html 224 @@ -7268,6 +7374,7 @@ Adjusted vsize + Prilagođena vveličina src/app/components/transaction/transaction.component.html 249 @@ -7277,6 +7384,7 @@ Locktime + Vrijeme zaključavanja src/app/components/transaction/transaction.component.html 271 @@ -7285,6 +7393,7 @@ Sigops + Sigops src/app/components/transaction/transaction.component.html 275 @@ -7294,6 +7403,7 @@ Transaction not found. + Transakcija nije pronađena. src/app/components/transaction/transaction.component.html 407 @@ -7302,6 +7412,7 @@ Waiting for it to appear in the mempool... + Čeka se da se pojavi u mempoolu... src/app/components/transaction/transaction.component.html 408 @@ -7310,6 +7421,7 @@ Error loading transaction data. + Pogreška pri učitavanju podataka o transakciji. src/app/components/transaction/transaction.component.html 414 @@ -7318,6 +7430,7 @@ Features + Značajke src/app/components/transaction/transaction.component.html 499 @@ -7335,6 +7448,7 @@ This transaction was projected to be included in the block + Predviđeno je da će ova transakcija biti uključena u blok src/app/components/transaction/transaction.component.html 522 @@ -7343,6 +7457,7 @@ Expected in Block + Očekuje se u bloku src/app/components/transaction/transaction.component.html 522 @@ -7352,6 +7467,7 @@ This transaction was seen in the mempool prior to mining + Ova transakcija je viđena u mempoolu prije rudarenja src/app/components/transaction/transaction.component.html 524 @@ -7360,6 +7476,7 @@ Seen in Mempool + Viđeno u Mempoolu src/app/components/transaction/transaction.component.html 524 @@ -7369,6 +7486,7 @@ This transaction was missing from our mempool prior to mining + Ova transakcija je nedostajala u našem mempoolu prije rudarenja src/app/components/transaction/transaction.component.html 526 @@ -7377,6 +7495,7 @@ Not seen in Mempool + Nije viđeno u Mempoolu src/app/components/transaction/transaction.component.html 526 @@ -7386,6 +7505,7 @@ This transaction may have been added out-of-band + Ova je transakcija možda dodana izvanpojasno src/app/components/transaction/transaction.component.html 529 @@ -7394,6 +7514,7 @@ This transaction may have been prioritized out-of-band + Ova transakcija je možda bila prioritizirana izvanpojasno src/app/components/transaction/transaction.component.html 532 @@ -7402,6 +7523,7 @@ This transaction conflicted with another version in our mempool + Ova transakcija bila je u sukobu s drugom verzijom u našem mempoolu src/app/components/transaction/transaction.component.html 535 @@ -7410,6 +7532,7 @@ (Newly Generated Coins) + (Novogenerirani coin-ovi) src/app/components/transactions-list/transactions-list.component.html 54 @@ -7418,6 +7541,7 @@ Peg-in + Peg-in src/app/components/transactions-list/transactions-list.component.html 56 @@ -7426,6 +7550,7 @@ ScriptSig (ASM) + ScriptSig (ASM) src/app/components/transactions-list/transactions-list.component.html 105 @@ -7435,6 +7560,7 @@ ScriptSig (HEX) + ScriptSig (HEX) src/app/components/transactions-list/transactions-list.component.html 109 @@ -7444,6 +7570,7 @@ Witness + Witness src/app/components/transactions-list/transactions-list.component.html 114 @@ -7452,6 +7579,7 @@ P2SH redeem script + P2SH otkupna skripta src/app/components/transactions-list/transactions-list.component.html 148 @@ -7460,6 +7588,7 @@ P2TR tapscript + P2TR tapscript src/app/components/transactions-list/transactions-list.component.html 152 @@ -7468,6 +7597,7 @@ P2WSH witness script + P2WSH witness skripta src/app/components/transactions-list/transactions-list.component.html 154 @@ -7476,6 +7606,7 @@ nSequence + nSequence src/app/components/transactions-list/transactions-list.component.html 168 @@ -7484,6 +7615,7 @@ Previous output script + Skripta prethodnog outputa src/app/components/transactions-list/transactions-list.component.html 173 @@ -7492,6 +7624,7 @@ Previous output type + Prethodna vrsta outputa src/app/components/transactions-list/transactions-list.component.html 177 @@ -7500,6 +7633,7 @@ Peg-out to + Peg-out u src/app/components/transactions-list/transactions-list.component.html 225,226 @@ -7508,6 +7642,7 @@ ScriptPubKey (ASM) + ScriptPubKey (ASM) src/app/components/transactions-list/transactions-list.component.html 284 @@ -7517,6 +7652,7 @@ ScriptPubKey (HEX) + ScriptPubKey (HEX) src/app/components/transactions-list/transactions-list.component.html 288 @@ -7526,6 +7662,7 @@ Show more inputs to reveal fee data + Prikaži više unosa za otkrivanje podataka o naknadama src/app/components/transactions-list/transactions-list.component.html 326 @@ -7534,6 +7671,7 @@ other inputs + drugi inputi src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 12 @@ -7542,6 +7680,7 @@ other outputs + drugi outputi src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 13 @@ -7550,6 +7689,7 @@ Input + Input src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 42 @@ -7562,6 +7702,7 @@ 1 block earlier + 1 blok ranije src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 123 @@ -7570,6 +7711,7 @@ 1 block later + 1 blok kasnije src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 127 @@ -7578,6 +7720,7 @@ in the same block + u istom bloku src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 131 @@ -7586,6 +7729,7 @@ blocks earlier + blokova ranije src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 137 @@ -7594,6 +7738,7 @@ spent + potrošeno src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 148 @@ -7602,6 +7747,7 @@ blocks later + blokova kasnije src/app/components/tx-bowtie-graph-tooltip/tx-bowtie-graph-tooltip.component.html 150 @@ -7610,6 +7756,7 @@ This transaction saved % on fees by using native SegWit + Ova je transakcija uštedjela % na naknadama korištenjem native SegWita src/app/components/tx-features/tx-features.component.html 2 @@ -7618,6 +7765,7 @@ SegWit + SegWit src/app/components/tx-features/tx-features.component.html 2 @@ -7635,6 +7783,7 @@ This transaction saved % on fees by using SegWit and could save % more by fully upgrading to native SegWit + Ova je transakcija uštedjela % na naknadama korištenjem SegWita i mogla bi uštedjeti % više potpunom nadogradnjom na native SegWit src/app/components/tx-features/tx-features.component.html 4 @@ -7643,6 +7792,7 @@ This transaction could save % on fees by upgrading to native SegWit or % by upgrading to SegWit-P2SH + Ova bi transakcija mogla uštedjeti % na naknadama nadogradnjom na native SegWit ili % nadogradnjom na SegWit-P2SH src/app/components/tx-features/tx-features.component.html 6 @@ -7651,6 +7801,7 @@ This transaction uses Taproot and thereby saved at least % on fees + Ova transakcija koristi Taproot i time je uštedjela najmanje % na naknadama src/app/components/tx-features/tx-features.component.html 12 @@ -7659,6 +7810,7 @@ This transaction uses Taproot and already saved at least % on fees, but could save an additional % by fully using Taproot + Ova transakcija koristi Taproot i već je uštedjela najmanje % na naknadama, ali bi mogla uštedjeti dodatnih % potpunom upotrebom Taproota src/app/components/tx-features/tx-features.component.html 14 @@ -7667,6 +7819,7 @@ This transaction could save % on fees by using Taproot + Ova bi transakcija mogla uštedjeti % na naknadama korištenjem Taproota src/app/components/tx-features/tx-features.component.html 16 @@ -7675,6 +7828,7 @@ This transaction does not use Taproot + Ova transakcija ne koristi Taproot src/app/components/tx-features/tx-features.component.html 18 @@ -7683,6 +7837,7 @@ This transaction uses Taproot + Ova transakcija koristi Taproot src/app/components/tx-features/tx-features.component.html 21 @@ -7691,6 +7846,7 @@ This transaction supports Replace-By-Fee (RBF) allowing fee bumping + Ova transakcija podržava Replace-By-Fee (RBF) što omogućuje povećanje naknada src/app/components/tx-features/tx-features.component.html 28 @@ -7699,6 +7855,7 @@ This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method + Ova transakcija NE podržava Replace-By-Fee (RBF) i ne može se povećati naknada ovom metodom src/app/components/tx-features/tx-features.component.html 29 @@ -7707,6 +7864,7 @@ Optimal + Optimalno src/app/components/tx-fee-rating/tx-fee-rating.component.html 1 @@ -7716,6 +7874,7 @@ Only ~ sat/vB was needed to get into this block + Za ulazak u ovaj blok bio je potreban samo ~ sat/vB src/app/components/tx-fee-rating/tx-fee-rating.component.html 2 @@ -7728,6 +7887,7 @@ Overpaid x + Preplaćeno x src/app/components/tx-fee-rating/tx-fee-rating.component.html 2 @@ -7741,6 +7901,7 @@ Liquid Federation Holdings + Holding Liquid Federacije src/app/dashboard/dashboard.component.html 165 @@ -7753,6 +7914,7 @@ Federation Timelock-Expired UTXOs + UTXO-i Federacije s isteklim vremenom zaključavanja src/app/dashboard/dashboard.component.html 174 @@ -7765,6 +7927,7 @@ L-BTC Supply Against BTC Holdings + Ponuda L-BTC-a protiv BTC Holdingsa src/app/dashboard/dashboard.component.html 184 @@ -7777,6 +7940,7 @@ Indexing in progress + Indeksiranje u tijeku src/app/dashboard/dashboard.component.html 364 @@ -7793,6 +7957,7 @@ mempool.space merely provides data about the Bitcoin network. It cannot help you with retrieving funds, wallet issues, etc.For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc). + mempool.space samo pruža podatke o Bitcoin mreži. Ne može vam pomoći s vraćanjem sredstava, problemima s novčanikom itd.Za sve takve zahtjeve morate se obratiti entitetu koji je pomogao u transakciji (softver za novčanik, mjenjačnica itd.). src/app/docs/api-docs/api-docs.component.html 15,16 @@ -7801,6 +7966,7 @@ REST API service + REST API usluga src/app/docs/api-docs/api-docs.component.html 50 @@ -7809,6 +7975,7 @@ Endpoint + Krajnja točka src/app/docs/api-docs/api-docs.component.html 60 @@ -7821,6 +7988,7 @@ Description + Opis src/app/docs/api-docs/api-docs.component.html 79 @@ -7836,6 +8004,7 @@ Default push: action: 'want', data: ['blocks', ...] to express what you want pushed. Available: blocks, mempool-blocks, live-2h-chart, and stats.Push transactions related to address: 'track-address': '3PbJ...bF9B' to receive all new transactions containing that address as input or output. Returns an array of transactions. address-transactions for new mempool transactions, and block-transactions for new block confirmed transactions. + Zadani push: akcija: 'want', data: ['blocks', ...] kako biste izrazili ono što želite da se progura. Dostupno: blokova, mempool-blokovi, live-2h-chart i statistika.Push transakcije povezane na adresu: 'track-address': '3PbJ...bF9B' za primanje svih novih transakcija koje sadrže tu adresu kao input ili output. Vraća niz transakcija. address-transactions za nove mempool transakcije i blok- transakcije za nove blok potvrđene transakcije. src/app/docs/api-docs/api-docs.component.html 120 @@ -7844,6 +8013,7 @@ Code Example + Primjer koda src/app/docs/code-template/code-template.component.html 6 @@ -7864,6 +8034,7 @@ Install Package + Instaliraj paket src/app/docs/code-template/code-template.component.html 23 @@ -7872,6 +8043,7 @@ Response + Odgovor src/app/docs/code-template/code-template.component.html 43 @@ -7880,6 +8052,7 @@ FAQ + FAQ src/app/docs/docs/docs.component.ts 46 @@ -7887,6 +8060,7 @@ Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more. + Dobijte odgovore na uobičajena pitanja poput: Što je mempool? Zašto se moja transakcija ne potvrđuje? Kako mogu pokrenuti vlastitu instancu projekta otvorenog koda Mempool? I više od toga. src/app/docs/docs/docs.component.ts 47 @@ -7894,6 +8068,7 @@ REST API + REST API src/app/docs/docs/docs.component.ts 51 @@ -7901,6 +8076,7 @@ Documentation for the liquid.network REST API service: get info on addresses, transactions, assets, blocks, and more. + Dokumentacija za REST API uslugu liquid.network: saznajte informacije o adresama, transakcijama, imovini, blokovima i više. src/app/docs/docs/docs.component.ts 53 @@ -7908,6 +8084,7 @@ Documentation for the mempool.space REST API service: get info on addresses, transactions, blocks, fees, mining, the Lightning network, and more. + Dokumentacija za mempool.space REST API uslugu: saznajte informacije o adresama, transakcijama, blokovima, naknadama, rudarenju, Lightning mreži i više. src/app/docs/docs/docs.component.ts 55 @@ -7915,6 +8092,7 @@ WebSocket API + WebSocket API src/app/docs/docs/docs.component.ts 59 @@ -7922,6 +8100,7 @@ Documentation for the liquid.network WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more. + Dokumentacija za liquid.network WebSocket API uslugu: dobijte informacije u stvarnom vremenu o blokovima, mempoolima, transakcijama, adresama i više. src/app/docs/docs/docs.component.ts 61 @@ -7929,6 +8108,7 @@ Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more. + Dokumentacija za mempool.space WebSocket API uslugu: dobijte informacije u stvarnom vremenu o blokovima, mempoolima, transakcijama, adresama i više. src/app/docs/docs/docs.component.ts 63 @@ -7936,6 +8116,7 @@ Electrum RPC + Electrum RPC src/app/docs/docs/docs.component.ts 67 @@ -7943,6 +8124,7 @@ Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance. + Dokumentacija za naše Electrum RPC sučelje: dobijte trenutan, praktičan i pouzdan pristup Esplora instanci. src/app/docs/docs/docs.component.ts 68 @@ -7950,6 +8132,7 @@ Base fee + Osnovna naknada src/app/lightning/channel/channel-box/channel-box.component.html 29 @@ -7962,6 +8145,7 @@ mSats + mSat src/app/lightning/channel/channel-box/channel-box.component.html 35 @@ -7982,6 +8166,7 @@ This channel supports zero base fee routing + Ovaj kanal podržava usmjeravanje bez osnovne naknade src/app/lightning/channel/channel-box/channel-box.component.html 44 @@ -7990,6 +8175,7 @@ Zero base fee + Nula osnovna naknada src/app/lightning/channel/channel-box/channel-box.component.html 45 @@ -7998,6 +8184,7 @@ This channel does not support zero base fee routing + Ovaj kanal ne podržava usmjeravanje bez osnovne naknade src/app/lightning/channel/channel-box/channel-box.component.html 50 @@ -8006,6 +8193,7 @@ Non-zero base fee + Osnovna naknada različita od nule src/app/lightning/channel/channel-box/channel-box.component.html 51 @@ -8014,6 +8202,7 @@ Min HTLC + Min HTLC src/app/lightning/channel/channel-box/channel-box.component.html 57 @@ -8022,6 +8211,7 @@ Max HTLC + Max HTLC src/app/lightning/channel/channel-box/channel-box.component.html 63 @@ -8030,6 +8220,7 @@ Timelock delta + Timelock delta src/app/lightning/channel/channel-box/channel-box.component.html 69 @@ -8038,6 +8229,7 @@ channels + kanala src/app/lightning/channel/channel-box/channel-box.component.html 79 @@ -8058,6 +8250,7 @@ Starting balance + Početno stanje src/app/lightning/channel/channel-close-box/channel-close-box.component.html 3 @@ -8067,6 +8260,7 @@ Closing balance + Završno stanje src/app/lightning/channel/channel-close-box/channel-close-box.component.html 26 @@ -8076,6 +8270,7 @@ lightning channel + lightning kanal src/app/lightning/channel/channel-preview.component.html 3 @@ -8084,6 +8279,7 @@ Inactive + Neaktivan src/app/lightning/channel/channel-preview.component.html 10 @@ -8100,6 +8296,7 @@ Active + Aktivan src/app/lightning/channel/channel-preview.component.html 11 @@ -8116,6 +8313,7 @@ Closed + Zatvoren src/app/lightning/channel/channel-preview.component.html 12 @@ -8140,6 +8338,7 @@ Created + Stvoren src/app/lightning/channel/channel-preview.component.html 23 @@ -8152,6 +8351,7 @@ Capacity + Kapacitet src/app/lightning/channel/channel-preview.component.html 27 @@ -8220,6 +8420,7 @@ ppm + ppm src/app/lightning/channel/channel-preview.component.html 34 @@ -8240,6 +8441,7 @@ Overview for Lightning channel . See channel capacity, the Lightning nodes involved, related on-chain transactions, and more. + Pregled za Lightning kanal . Pogledajte kapacitet kanala, uključene Lightning nodove, povezane transakcije on-chain i još mnogo toga. src/app/lightning/channel/channel-preview.component.ts 37 @@ -8251,6 +8453,7 @@ Lightning channel + Lightning kanal src/app/lightning/channel/channel.component.html 4 @@ -8263,6 +8466,7 @@ Last update + Zadnje ažuriranje src/app/lightning/channel/channel.component.html 40 @@ -8295,6 +8499,7 @@ Closing date + Datum zatvaranja src/app/lightning/channel/channel.component.html 44 @@ -8307,6 +8512,7 @@ Closed by + Zatvorio src/app/lightning/channel/channel.component.html 59 @@ -8315,6 +8521,7 @@ Opening transaction + Transakcija otvaranja src/app/lightning/channel/channel.component.html 91 @@ -8327,6 +8534,7 @@ Closing transaction + Transakcija zatvaranja src/app/lightning/channel/channel.component.html 100 @@ -8339,6 +8547,7 @@ Channel: + Kanal: src/app/lightning/channel/channel.component.ts 37 @@ -8346,6 +8555,7 @@ Mutually closed + Međusobno zatvoren src/app/lightning/channel/closing-type/closing-type.component.ts 20 @@ -8353,6 +8563,7 @@ Force closed + Prisilno zatvoren src/app/lightning/channel/closing-type/closing-type.component.ts 24 @@ -8360,6 +8571,7 @@ Force closed with penalty + Prisilno zatvoren s kaznom src/app/lightning/channel/closing-type/closing-type.component.ts 28 @@ -8367,6 +8579,7 @@ Open + Otvoren src/app/lightning/channels-list/channels-list.component.html 5 @@ -8375,6 +8588,7 @@ No channels to display + Nema kanala za prikaz src/app/lightning/channels-list/channels-list.component.html 29 @@ -8383,6 +8597,7 @@ Alias + Alias src/app/lightning/channels-list/channels-list.component.html 38 @@ -8419,6 +8634,7 @@ Channel ID + ID kanala src/app/lightning/channels-list/channels-list.component.html 44 @@ -8431,6 +8647,7 @@ avg + prosj src/app/lightning/channels-statistics/channels-statistics.component.html 3 @@ -8439,6 +8656,7 @@ med + med src/app/lightning/channels-statistics/channels-statistics.component.html 6 @@ -8447,6 +8665,7 @@ Avg Capacity + Prosječni kapacitet src/app/lightning/channels-statistics/channels-statistics.component.html 13 @@ -8459,6 +8678,7 @@ Avg Fee Rate + Prosječna stopa naknade src/app/lightning/channels-statistics/channels-statistics.component.html 26 @@ -8471,6 +8691,7 @@ The average fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Prosječna stopa naknade koju naplaćuju usmjerivački nodovi, zanemarujući stope naknade > 0,5% ili 5000ppm src/app/lightning/channels-statistics/channels-statistics.component.html 28 @@ -8479,6 +8700,7 @@ Avg Base Fee + Prosječna osnovna naknada src/app/lightning/channels-statistics/channels-statistics.component.html 41 @@ -8491,6 +8713,7 @@ The average base fee charged by routing nodes, ignoring base fees > 5000ppm + Prosječna osnovna naknada koju naplaćuju usmjerivački nodovi, zanemarujući osnovne naknade > 5000ppm src/app/lightning/channels-statistics/channels-statistics.component.html 43 @@ -8499,6 +8722,7 @@ Med Capacity + Medijalni Kapacitet src/app/lightning/channels-statistics/channels-statistics.component.html 59 @@ -8507,6 +8731,7 @@ Med Fee Rate + Medijalna stopa naknade src/app/lightning/channels-statistics/channels-statistics.component.html 72 @@ -8515,6 +8740,7 @@ The median fee rate charged by routing nodes, ignoring fee rates > 0.5% or 5000ppm + Medijalna stopa naknade koju naplaćuju usmjerivački nodovi, zanemarujući stope naknada > 0,5% ili 5000ppm src/app/lightning/channels-statistics/channels-statistics.component.html 74 @@ -8523,6 +8749,7 @@ Med Base Fee + Medijalna osnovna naknada src/app/lightning/channels-statistics/channels-statistics.component.html 87 @@ -8531,6 +8758,7 @@ The median base fee charged by routing nodes, ignoring base fees > 5000ppm + Medijalna osnovna naknada koju naplaćuju usmjerivački nodovi, zanemarujući osnovne naknade > 5000ppm src/app/lightning/channels-statistics/channels-statistics.component.html 89 @@ -8539,6 +8767,7 @@ Lightning node group + Grupa lightning nodova src/app/lightning/group/group-preview.component.html 3 @@ -8551,6 +8780,7 @@ Nodes + Nodovi src/app/lightning/group/group-preview.component.html 25 @@ -8591,6 +8821,7 @@ Liquidity + Likvidnost src/app/lightning/group/group-preview.component.html 29 @@ -8623,6 +8854,7 @@ Channels + Kanali src/app/lightning/group/group-preview.component.html 40 @@ -8695,6 +8927,7 @@ Average size + Prosječna veličina src/app/lightning/group/group-preview.component.html 44 @@ -8707,6 +8940,7 @@ Connect + Poveži se src/app/lightning/group/group.component.html 73 @@ -8716,6 +8950,7 @@ Location + Mjesto src/app/lightning/group/group.component.html 74 @@ -8756,6 +8991,7 @@ Penalties + Penali src/app/lightning/justice-list/justice-list.component.html 4 @@ -8764,6 +9000,7 @@ Network Statistics + Statistika mreže src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 10 @@ -8772,6 +9009,7 @@ Channels Statistics + Statistika kanala src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 24 @@ -8780,6 +9018,7 @@ Lightning Network History + Povijest Lightning mreže src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 52 @@ -8788,6 +9027,7 @@ Liquidity Ranking + Rangiranje likvidnosti src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 66 @@ -8808,6 +9048,7 @@ Connectivity Ranking + Rangiranje povezanosti src/app/lightning/lightning-dashboard/lightning-dashboard.component.html 80 @@ -8828,6 +9069,7 @@ Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc). + Dobijte statistiku o Lightning mreži (ukupni kapacitet, povezanost itd.), Lightning nodovima (kanali, likvidnost itd.) i Lightning kanalima (status, naknade itd.). src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts 34 @@ -8835,6 +9077,7 @@ Fee distribution + Distribucija naknada src/app/lightning/node-fee-chart/node-fee-chart.component.html 2 @@ -8843,6 +9086,7 @@ Outgoing Fees + Odlazne naknade src/app/lightning/node-fee-chart/node-fee-chart.component.ts 179 @@ -8854,6 +9098,7 @@ Incoming Fees + Ulazne naknade src/app/lightning/node-fee-chart/node-fee-chart.component.ts 187 @@ -8865,6 +9110,7 @@ Percentage change past week + Postotna promjena prošli tjedan src/app/lightning/node-statistics/node-statistics.component.html 5 @@ -8881,6 +9127,7 @@ Lightning node + Lightning nod src/app/lightning/node/node-preview.component.html 3 @@ -8897,6 +9144,7 @@ Active capacity + Aktivni kapacitet src/app/lightning/node/node-preview.component.html 20 @@ -8909,6 +9157,7 @@ Active channels + Aktivni kanali src/app/lightning/node/node-preview.component.html 26 @@ -8921,6 +9170,7 @@ Country + Zemlja src/app/lightning/node/node-preview.component.html 44 @@ -8929,6 +9179,7 @@ Overview for the Lightning network node named . See channels, capacity, location, fee stats, and more. + Pregled Lightning noda pod nazivom . Pogledajte kanale, kapacitet, lokaciju, statistiku naknada i još mnogo toga. src/app/lightning/node/node-preview.component.ts 52 @@ -8940,6 +9191,7 @@ Average channel size + Prosječna veličina kanala src/app/lightning/node/node.component.html 44 @@ -8948,6 +9200,7 @@ Avg channel distance + Prosječna udaljenost kanala src/app/lightning/node/node.component.html 60 @@ -8956,6 +9209,7 @@ Color + Boja src/app/lightning/node/node.component.html 86 @@ -8964,6 +9218,7 @@ ISP + ISP src/app/lightning/node/node.component.html 92 @@ -8976,6 +9231,7 @@ Exclusively on Tor + Isključivo na Tor-u src/app/lightning/node/node.component.html 100 @@ -8984,6 +9240,7 @@ Decoded + Dekodirano src/app/lightning/node/node.component.html 134 @@ -8993,6 +9250,7 @@ Liquidity ad + Oglas za likvidnost src/app/lightning/node/node.component.html 184 @@ -9001,6 +9259,7 @@ Lease fee rate + Lease stopa naknade src/app/lightning/node/node.component.html 190 @@ -9010,6 +9269,7 @@ Lease base fee + Lease osnovna naknada src/app/lightning/node/node.component.html 198 @@ -9018,6 +9278,7 @@ Funding weight + Težina financiranja src/app/lightning/node/node.component.html 204 @@ -9026,6 +9287,7 @@ Channel fee rate + Stopa naknade za kanal src/app/lightning/node/node.component.html 214 @@ -9035,6 +9297,7 @@ Channel base fee + Osnovna naknada kanala src/app/lightning/node/node.component.html 222 @@ -9043,6 +9306,7 @@ Compact lease + Compact lease src/app/lightning/node/node.component.html 234 @@ -9051,6 +9315,7 @@ TLV extension records + TLV extension records src/app/lightning/node/node.component.html 245 @@ -9059,6 +9324,7 @@ Open channels + Otvoreni kanali src/app/lightning/node/node.component.html 286 @@ -9067,6 +9333,7 @@ Closed channels + Zatvoreni kanali src/app/lightning/node/node.component.html 290 @@ -9075,6 +9342,7 @@ Node: + Nod: src/app/lightning/node/node.component.ts 63 @@ -9082,6 +9350,7 @@ (Tor nodes excluded) + (Tor nodovi su isključeni) src/app/lightning/nodes-channels-map/nodes-channels-map.component.html 22 @@ -9102,6 +9371,7 @@ Lightning Nodes Channels World Map + Karta svijeta kanala Lightning nodova src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 73 @@ -9109,6 +9379,7 @@ See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Pogledajte kanale Lightning nodova koji nisu Tor, vizualizirani na karti svijeta. Zadržite pokazivač/dodirnite točke na karti za nazive nodova i detalje. src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 74 @@ -9116,6 +9387,7 @@ No geolocation data available + Nema dostupnih geolokacijskih podataka src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 245 @@ -9123,6 +9395,7 @@ Active channels map + Karta aktivnih kanala src/app/lightning/nodes-channels/node-channels.component.html 3 @@ -9131,6 +9404,7 @@ See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Pogledajte lokacije Lightning nodova koji nisu Tor vizualizirane na karti svijeta. Zadržite pokazivač/dodirnite točke na karti za nazive nodova i detalje. src/app/lightning/nodes-map/nodes-map.component.ts 52 @@ -9138,6 +9412,7 @@ See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both. + Pogledajte broj Lightning nodova vizualiziranih tijekom vremena po mreži: samo clearnet (IPv4, IPv6), darknet (Tor, I2p, cjdns) i oboje. src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 74 @@ -9145,6 +9420,7 @@ Indexing in progress + Indeksiranje u tijeku src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 133 @@ -9156,6 +9432,7 @@ Clearnet and Darknet + Clearnet i Darknet src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 176 @@ -9167,6 +9444,7 @@ Clearnet Only (IPv4, IPv6) + Samo Clearnet (IPv4, IPv6) src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 197 @@ -9178,6 +9456,7 @@ Darknet Only (Tor, I2P, cjdns) + Samo Darknet (Tor, I2P, cjdns) src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 218 @@ -9189,6 +9468,7 @@ Share + Podijeli src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html 29 @@ -9201,6 +9481,7 @@ See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more. + Pogledajte zemljopisnu analizu Lightning mreže: koliko Lightning nodova je hostirano u zemljama diljem svijeta, ukupni BTC kapacitet za svaku zemlju i više. src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 47 @@ -9208,6 +9489,7 @@ nodes + nodova src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 104 @@ -9227,6 +9509,7 @@ BTC capacity + BTC kapacitet src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 105 @@ -9234,6 +9517,7 @@ Lightning nodes in + Lightning nodova u src/app/lightning/nodes-per-country/nodes-per-country.component.html 3,4 @@ -9242,6 +9526,7 @@ ISP Count + Broj ISP-a src/app/lightning/nodes-per-country/nodes-per-country.component.html 34 @@ -9250,6 +9535,7 @@ Top ISP + Najbolji ISP src/app/lightning/nodes-per-country/nodes-per-country.component.html 38 @@ -9258,6 +9544,7 @@ Lightning nodes in + Lightning nodovi u src/app/lightning/nodes-per-country/nodes-per-country.component.ts 43 @@ -9265,6 +9552,7 @@ Explore all the Lightning nodes hosted in and see an overview of each node's capacity, number of open channels, and more. + Istražite sve Lightning nodove hostirane u i pogledajte pregled kapaciteta svakog noda, broj otvorenih kanala i više. src/app/lightning/nodes-per-country/nodes-per-country.component.ts 44 @@ -9272,6 +9560,7 @@ Clearnet Capacity + Clearnet kapacitet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 6 @@ -9284,6 +9573,7 @@ How much liquidity is running on nodes advertising at least one clearnet IP address + Koliko likvidnosti se pokreće na nodovima koji oglašavaju barem jednu clearnet IP adresu src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 8 @@ -9292,6 +9582,7 @@ Unknown Capacity + Nepoznati kapacitet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 13 @@ -9304,6 +9595,7 @@ How much liquidity is running on nodes which ISP was not identifiable + Koliko likvidnosti teče na nodovima čiji ISP nije bio identificiran src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 15 @@ -9312,6 +9604,7 @@ Tor Capacity + Tor kapacitet src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 20 @@ -9324,6 +9617,7 @@ How much liquidity is running on nodes advertising only Tor addresses + Koliko se likvidnosti pokreće na nodovima koji oglašavaju samo Tor adrese src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 22 @@ -9332,6 +9626,7 @@ Top 100 ISPs hosting LN nodes + 100 najboljih ISP-ova koji hostiraju LN nodove src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html 31 @@ -9340,6 +9635,7 @@ Browse the top 100 ISPs hosting Lightning nodes along with stats like total number of nodes per ISP, aggregate BTC capacity per ISP, and more + Pregledajte 100 najboljih ISP-ova koji hostiraju Lightning nodove zajedno sa statistikama kao što je ukupan broj nodova po ISP-u, ukupni BTC kapacitet po ISP-u i više src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 54 @@ -9347,6 +9643,7 @@ BTC + BTC src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 164 @@ -9358,6 +9655,7 @@ Lightning ISP + Lightning ISP src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 3 @@ -9366,6 +9664,7 @@ Top country + Naj država src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 39 @@ -9378,6 +9677,7 @@ Top node + Naj nod src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html 45 @@ -9386,6 +9686,7 @@ Lightning nodes on ISP: [AS] + Lightning nodovi na ISP-u: [AS] src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 44 @@ -9397,6 +9698,7 @@ Browse all Bitcoin Lightning nodes using the [AS] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP. + Pregledajte sve Bitcoin Lightning nodove pomoću [AS] ISP-a i pogledajte skupne statistike poput ukupnog broja nodova, ukupnog kapaciteta, i više za ISP-a. src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 45 @@ -9408,6 +9710,7 @@ Lightning nodes on ISP: + Lightning nodovi na ISP-u: src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 2,4 @@ -9416,6 +9719,7 @@ ASN + ASN src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 10 @@ -9424,6 +9728,7 @@ Active nodes + Aktivni nodovi src/app/lightning/nodes-per-isp/nodes-per-isp.component.html 14 @@ -9432,6 +9737,7 @@ Top 100 oldest lightning nodes + Top 100 najstarijih lightning nodova src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html 3 @@ -9440,6 +9746,7 @@ Oldest lightning nodes + Najstariji lightning nodovi src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 27 @@ -9447,6 +9754,7 @@ See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc. + Pogledajte najstarije nodove na Lightning mreži zajedno s njihovim kapacitetom, brojem kanala, lokacijom itd. src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 28 @@ -9454,6 +9762,7 @@ See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more. + Pogledajte Lightning nodove s najvećom likvidnošću BTC-a zajedno sa statističkim podacima na visokoj razini kao što su broj otvorenih kanala, lokacija, starost noda i više. src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts 35 @@ -9461,6 +9770,7 @@ See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more. + Pogledajte Lightning nodove s najviše otvorenih kanala zajedno sa statistikama visoke razine kao što su ukupni kapacitet noda, starost noda i više. src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts 39 @@ -9468,6 +9778,7 @@ Oldest nodes + Najstariji nodovi src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html 36 @@ -9476,6 +9787,7 @@ Top lightning nodes + Top lightning nodovi src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 22 @@ -9483,6 +9795,7 @@ See the top Lightning network nodes ranked by liquidity, connectivity, and age. + Pogledajte najbolje nodove Lightning mreže rangirane prema likvidnosti, povezanosti i starosti. src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts 23 @@ -9490,6 +9803,7 @@ See the capacity of the Lightning network visualized over time in terms of the number of open channels and total bitcoin capacity. + Pogledajte vizualizaciju kapaciteta Lightning mreže kroz vrijeme u smislu broja otvorenih kanala i ukupnog kapaciteta bitcoina. src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts 71 @@ -9497,6 +9811,7 @@ fee + naknada src/app/shared/components/address-type/address-type.component.html 3 @@ -9505,6 +9820,7 @@ empty + prazno src/app/shared/components/address-type/address-type.component.html 6 @@ -9513,6 +9829,7 @@ provably unspendable + dokazivo nepotrošivo src/app/shared/components/address-type/address-type.component.html 18 @@ -9521,6 +9838,7 @@ bare multisig + goli multisig src/app/shared/components/address-type/address-type.component.html 21 @@ -9529,6 +9847,7 @@ confirmation + potvrda src/app/shared/components/confirmations/confirmations.component.html 4 @@ -9538,6 +9857,7 @@ confirmations + potvrde src/app/shared/components/confirmations/confirmations.component.html 5 @@ -9547,6 +9867,7 @@ Replaced + Zamijenjeno src/app/shared/components/confirmations/confirmations.component.html 12 @@ -9566,6 +9887,7 @@ sat/WU + sat/WU src/app/shared/components/fee-rate/fee-rate.component.html 4 @@ -9579,6 +9901,7 @@ My Account + Moj račun src/app/shared/components/global-footer/global-footer.component.html 36 @@ -9591,6 +9914,7 @@ Explore + Istraži src/app/shared/components/global-footer/global-footer.component.html 59 @@ -9599,6 +9923,7 @@ Test Transaction + Testna transakcija src/app/shared/components/global-footer/global-footer.component.html 64 @@ -9608,6 +9933,7 @@ Connect to our Nodes + Povežite se s našim nodovima src/app/shared/components/global-footer/global-footer.component.html 65 @@ -9616,6 +9942,7 @@ API Documentation + API dokumentacija src/app/shared/components/global-footer/global-footer.component.html 66 @@ -9624,6 +9951,7 @@ Learn + Nauči src/app/shared/components/global-footer/global-footer.component.html 69 @@ -9632,6 +9960,7 @@ What is a mempool? + Što je mempool? src/app/shared/components/global-footer/global-footer.component.html 70 @@ -9640,6 +9969,7 @@ What is a block explorer? + Što je blok explorer? src/app/shared/components/global-footer/global-footer.component.html 71 @@ -9648,6 +9978,7 @@ What is a mempool explorer? + Što je mempool explorer? src/app/shared/components/global-footer/global-footer.component.html 72 @@ -9656,6 +9987,7 @@ Why isn't my transaction confirming? + Zašto se moja transakcija ne potvrđuje? src/app/shared/components/global-footer/global-footer.component.html 73 @@ -9664,6 +9996,7 @@ More FAQs » + Više često postavljanih pitanja » src/app/shared/components/global-footer/global-footer.component.html 74 @@ -9672,6 +10005,7 @@ Research + Istraživanje src/app/shared/components/global-footer/global-footer.component.html 75 @@ -9680,6 +10014,7 @@ Networks + Mreže src/app/shared/components/global-footer/global-footer.component.html 79 @@ -9688,6 +10023,7 @@ Mainnet Explorer + Mainnet Explorer src/app/shared/components/global-footer/global-footer.component.html 80 @@ -9696,6 +10032,7 @@ Testnet3 Explorer + Testnet3 Explorer src/app/shared/components/global-footer/global-footer.component.html 81 @@ -9704,6 +10041,7 @@ Testnet4 Explorer + Testnet4 Explorer src/app/shared/components/global-footer/global-footer.component.html 82 @@ -9712,6 +10050,7 @@ Signet Explorer + Signet Explorer src/app/shared/components/global-footer/global-footer.component.html 83 @@ -9720,6 +10059,7 @@ Liquid Testnet Explorer + Liquid Testnet Explorer src/app/shared/components/global-footer/global-footer.component.html 84 @@ -9728,6 +10068,7 @@ Liquid Explorer + Liquid Explorer src/app/shared/components/global-footer/global-footer.component.html 85 @@ -9736,6 +10077,7 @@ Tools + Alati src/app/shared/components/global-footer/global-footer.component.html 89 @@ -9744,6 +10086,7 @@ Clock (Mined) + Sat (izrudaren) src/app/shared/components/global-footer/global-footer.component.html 91 @@ -9752,6 +10095,7 @@ Legal + Legalno src/app/shared/components/global-footer/global-footer.component.html 96 @@ -9760,6 +10104,7 @@ Terms of Service + Uvjeti usluge src/app/shared/components/global-footer/global-footer.component.html 97 @@ -9769,6 +10114,7 @@ Privacy Policy + Politika privatnosti src/app/shared/components/global-footer/global-footer.component.html 98 @@ -9778,6 +10124,7 @@ Trademark Policy + Politika zaštitnih znakova src/app/shared/components/global-footer/global-footer.component.html 99 @@ -9787,6 +10134,7 @@ Third-party Licenses + Licence trećih strana src/app/shared/components/global-footer/global-footer.component.html 100 @@ -9796,6 +10144,7 @@ Your balance is too low.Please top up your account. + Vaš saldo je prenizak.Molimo dopunite svoj račun . src/app/shared/components/mempool-error/mempool-error.component.html 9 @@ -9804,6 +10153,7 @@ This is a test network. Coins have no value. + Ovo je testna mreža. Coini nemaju vrijednost. src/app/shared/components/testnet-alert/testnet-alert.component.html 4 @@ -9812,6 +10162,7 @@ Testnet3 is deprecated, and will soon be replaced by Testnet4 + Testnet3 je zastario i uskoro će ga zamijeniti Testnet4 src/app/shared/components/testnet-alert/testnet-alert.component.html 6 @@ -9820,6 +10171,7 @@ Testnet4 is not yet finalized, and may be reset at anytime. + Testnet4 još nije finaliziran i može se resetirati u bilo kojem trenutku. src/app/shared/components/testnet-alert/testnet-alert.component.html 9 @@ -9828,6 +10180,7 @@ Batch payment + Skupno plaćanje src/app/shared/filters.utils.ts 108 @@ -9835,6 +10188,7 @@ Address Types + Vrste adresa src/app/shared/filters.utils.ts 119 @@ -9842,6 +10196,7 @@ Behavior + Ponašanje src/app/shared/filters.utils.ts 120 @@ -9849,6 +10204,7 @@ Heuristics + Heuristika src/app/shared/filters.utils.ts 122 @@ -9856,6 +10212,7 @@ Sighash Flags + Sighash Flags src/app/shared/filters.utils.ts 123 @@ -9863,6 +10220,7 @@ year + godina src/app/shared/i18n/dates.ts 3 @@ -9870,6 +10228,7 @@ years + godina src/app/shared/i18n/dates.ts 4 @@ -9877,6 +10236,7 @@ month + mjesec src/app/shared/i18n/dates.ts 5 @@ -9884,6 +10244,7 @@ months + mjeseci src/app/shared/i18n/dates.ts 6 @@ -9891,6 +10252,7 @@ week + tjedan src/app/shared/i18n/dates.ts 7 @@ -9898,6 +10260,7 @@ weeks + tjedana src/app/shared/i18n/dates.ts 8 @@ -9905,6 +10268,7 @@ day + dan src/app/shared/i18n/dates.ts 9 @@ -9912,6 +10276,7 @@ days + dana src/app/shared/i18n/dates.ts 10 @@ -9919,6 +10284,7 @@ hour + sat src/app/shared/i18n/dates.ts 11 @@ -9926,6 +10292,7 @@ hours + sati src/app/shared/i18n/dates.ts 12 @@ -9933,6 +10300,7 @@ minute + minuta src/app/shared/i18n/dates.ts 13 @@ -9940,6 +10308,7 @@ minutes + minuta src/app/shared/i18n/dates.ts 14 @@ -9947,6 +10316,7 @@ second + sekunda src/app/shared/i18n/dates.ts 15 @@ -9954,6 +10324,7 @@ seconds + sekundi src/app/shared/i18n/dates.ts 16 @@ -9961,6 +10332,7 @@ Transaction fee + Transakcijska naknada src/app/shared/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts 11 @@ -9968,6 +10340,7 @@ Multisig of + Multisig od src/app/shared/script.utils.ts 168 diff --git a/frontend/src/locale/messages.tr.xlf b/frontend/src/locale/messages.tr.xlf index 95c6222e4..7912e34ee 100644 --- a/frontend/src/locale/messages.tr.xlf +++ b/frontend/src/locale/messages.tr.xlf @@ -645,6 +645,7 @@ This will reduce your expected waiting time until the first confirmation to + İlk onaya kadar geçen bekleme süresini kadar azaltacak. src/app/components/accelerate-checkout/accelerate-checkout.component.html 76,77 @@ -1392,6 +1393,7 @@ Out-of-band fees + Bant-dışı ücretler src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html 27 @@ -1791,6 +1793,7 @@ Completed + Tamamlandı src/app/components/acceleration/accelerations-list/accelerations-list.component.html 65 @@ -1799,6 +1802,7 @@ Failed + Başarısız oldu src/app/components/acceleration/accelerations-list/accelerations-list.component.html 67 @@ -2320,6 +2324,7 @@ There are too many transactions on this address, more than your backend can handle. See more on setting up a stronger backend. Consider viewing this address on the official Mempool website instead: + Bu adres üzerindeki işlem sayısı arka arayüzününüzün işleyemeyeceği kadar fazla. Daha kuvvetli bir arkayüz için 'ye bakın. . Ya da bu adresi resmi Mempool sitesinde görüntüleyin: src/app/components/address/address.component.html 204,207 @@ -2535,6 +2540,7 @@ Browse an overview of the Liquid asset (): see issued amount, burned amount, circulating amount, related transactions, and more. + Liquid varlığın genel görünümünü incele (): üretilen, yakılan, dolaşan miktarlır ve ilişkili işlemleri ve daha fazlasını gör. src/app/components/asset/asset.component.ts 108 @@ -2800,6 +2806,7 @@ See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles. + Bitcoin ücret çizelgesinin zaman içindeki değişimini görüntüle. Minimum ve maksimum ücretler ve farklı yüzdelik dilimlerdeki ücretleri görüntüleyebilirsin. src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 73 @@ -2824,6 +2831,7 @@ See the average mining fees earned per Bitcoin block visualized in BTC and USD over time. + Bitcoin bloğu başına ortalama madencilik ücretlerinin BTC ve USD cinsi olarak değişimini gör. src/app/components/block-fees-graph/block-fees-graph.component.ts 70 @@ -3012,6 +3020,7 @@ See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm. + Bitcoin blok sağlığını zaman içinde görüntüle. Blok sağlığı beklenen işlemlerin kaçının gerçekten bloğa dahil edildiğinin ölçüsüdür. Beklenen işlemler Mempool'un çalıştırdığı Bitcoin Core işlem seçme algoritması ile belirlenir. src/app/components/block-health-graph/block-health-graph.component.ts 64 @@ -3298,6 +3307,7 @@ See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees. + Bitcoin blok ödüllerini BTC ve USD cinsinden zaman içerisinde görüntüle. Blok ödülleri yeni çıkarılan bitcoin ödülleri ve işlem ücretlerinin toplamıdır. src/app/components/block-rewards-graph/block-rewards-graph.component.ts 68 @@ -3322,6 +3332,7 @@ See Bitcoin block sizes (MB) and block weights (weight units) visualized over time. + Bitcoin blok boyutlarını (MB) ve blok ağırlıklarını (ağırlık ünitesi) zaman içinde görselleştir. src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts 65 @@ -3445,6 +3456,7 @@ See size, weight, fee range, included transactions, and more for Liquid block (). + Liquid bloğundaki () boyut, ağırlık, ücret aralığı, dahil edilen işlemler ve daha fazlasını gör. src/app/components/block-view/block-view.component.ts 112 @@ -3460,6 +3472,7 @@ See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin block (). + Bitcoin block () için boyut, ağırlıklar, ücret aralığı, dahili işlemler, denetim (beklene vs gerçek) ve daha fazlasını gör. src/app/components/block-view/block-view.component.ts 114 @@ -3651,6 +3664,7 @@ This block does not belong to the main chain, it has been replaced by: + Bu blok ana-zincire dahil değil ve şununla değiştirilebilir: src/app/components/block/block.component.html 5 @@ -4173,6 +4187,7 @@ See the most recent Liquid blocks along with basic stats such as block height, block size, and more. + En güncel Liquid blokları için blok yüksekliği, blok büyüklüğü vb temel dataları gör. src/app/components/blocks-list/blocks-list.component.ts 71 @@ -4180,6 +4195,7 @@ See the most recent Bitcoin blocks along with basic stats such as block height, block reward, block size, and more. + En güncel Bitcoin blokları için blok yüksekliği, blok büyüklüğü vb temel dataları gör. src/app/components/blocks-list/blocks-list.component.ts 73 @@ -5162,6 +5178,7 @@ See hashrate and difficulty for the Bitcoin network visualized over time. + Bitcoin ağı için hashrate ve zorluk seviyelerinin değişimini zaman içinde gör. src/app/components/hashrate-chart/hashrate-chart.component.ts 76 @@ -5189,6 +5206,7 @@ See Bitcoin mining pool dominance visualized over time: see how top mining pools' share of total hashrate has fluctuated over time. + Madencilik havuzu dominasyonunu değişimini zaman içinde gör : en büyük madencilik havuzlarının toplam havuzdan aldığı payın değişimini incele. src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts 75 @@ -5311,6 +5329,7 @@ Total amount of BTC held in non-dust Federation UTXOs that have expired timelocks + Dust-dışı Federasyon UTXO'larındaki zaman kilidi bitmiş toplam BTC miktarını gör. src/app/components/liquid-reserves-audit/expired-utxos-stats/expired-utxos-stats.component.html 5 @@ -5510,6 +5529,7 @@ Fund / Redemption Tx + Fon/ Amortisman İşlemi src/app/components/liquid-reserves-audit/recent-pegs-list/recent-pegs-list.component.html 15 @@ -5581,6 +5601,7 @@ Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply + Federasyonun tuttuğu BTC miktarının toplam L-BTC'nin %95'inin altına düşme sayısı src/app/components/liquid-reserves-audit/reserves-ratio-stats/reserves-ratio-stats.component.html 6 @@ -5698,6 +5719,7 @@ See stats for transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions. + İşlemler için mempool istatistiklerini göster: ücret aralığı, toplam büyüklük, ve fazlasını gör. Mempool blokları, ağa yeni işlem geldiğinde anlık olarak güncellenir. src/app/components/mempool-block/mempool-block.component.ts 62 @@ -5793,6 +5815,7 @@ Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more. + Anlık olarak hashrate, zorluk seviyesi, blok ödülleri, havuz dominasyonu vb madencilik istatistiklerini görüntüle. src/app/components/mining-dashboard/mining-dashboard.component.ts 30 @@ -6071,6 +6094,7 @@ See mining pool stats for : most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more. + Madencilik havuzu istatistiklerini : en son bulunan bloklar, hashrate'in zaman içindeki değişimi, bugüne kadarki toplam ödül miktarı, bilinen Coinbase adresleri vb gör. src/app/components/pool/pool-preview.component.ts 86 @@ -6305,6 +6329,7 @@ See the most recent RBF replacements on the Bitcoin network, updated in real-time. + Bitcoin ağı üzerindeki en yeni RBF değişimlerini gerçek zamanlı olarak görüntüle. src/app/components/rbf-list/rbf-list.component.ts 62 @@ -6618,6 +6643,7 @@ Cap outliers + Sınır dışı değerler src/app/components/statistics/statistics.component.html 121 @@ -6626,6 +6652,7 @@ See mempool size (in MvB) and transactions per second (in vB/s) visualized over time. + Mempool büyüklüğünün (MvB olarak) ve saniyedeki işlem sayısının (vB/s) zaman içindeki değişimini görselleştir. src/app/components/statistics/statistics.component.ts 66 @@ -6633,6 +6660,7 @@ See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV. + Bitcoin bloklarını ve mempool yoğunluğunu televizyon formatına uygun olarak doğru zamanlı gör src/app/components/television/television.component.ts 40 @@ -6667,6 +6695,7 @@ Comma-separated list of raw transactions + Raw-işlem datalarının virgül ile ayrık gösterimi src/app/components/test-transactions/test-transactions.component.html 7 @@ -7113,6 +7142,7 @@ Waiting for your transaction to appear in the mempool + İşleminizin mempool'da gözükemsini bekliyoruz. src/app/components/tracker/tracker.component.html 150 @@ -7121,6 +7151,7 @@ Your transaction is in the mempool, but it will not be confirmed for some time. + İşleminiz mempool'da yalnız yakın zamanda onaylanması beklenmiyor. src/app/components/tracker/tracker.component.html 156 @@ -7129,6 +7160,7 @@ Your transaction is near the top of the mempool, and is expected to confirm soon. + İşleminizin mempool'un üst kademesinde, yakında onaylanması bekleniyor. src/app/components/tracker/tracker.component.html 162 @@ -7137,6 +7169,7 @@ Your transaction is expected to confirm in the next block + İşleminizin bir sonraki blokta onaylanması bekleniyor. src/app/components/tracker/tracker.component.html 168 @@ -7188,6 +7221,7 @@ Get real-time status, addresses, fees, script info, and more for transaction with txid . + İşlemler ve işlem id'si için anlık durum, adresler, ücretler, script vb bilgileri çek. src/app/components/tracker/tracker.component.ts 413 @@ -7923,6 +7957,7 @@ mempool.space merely provides data about the Bitcoin network. It cannot help you with retrieving funds, wallet issues, etc.For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc). + mempool.space Bitcoin ağı hakkında sadece bilgi sağlar. kaybettiğiniz fonları, cüzdanlar ile yaşadığınız sorunları çözmekte yardımcı olamaz. İşlemler ile ilgili sorun yaşarsanız bu işlemi gerçekleştirdiğiniz entite ile iletişime geçmeniz gerekir. (cüzdan yazılımı, borsa vb) src/app/docs/api-docs/api-docs.component.html 15,16 @@ -8025,6 +8060,7 @@ Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more. + Mempool nedir, neden işlemim onaylanmıyor, Açık Kaynak Kodlu Mempool projesinin bir kopyasını nasıl çalıştırabilirim? gibi temel sorulara cevaplar bulun. src/app/docs/docs/docs.component.ts 47 @@ -8072,6 +8108,7 @@ Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more. + Mempool.space Websoket API servisi için, bloklardan gerçek-zamanlı bilgi çek, mempoollar, işlemler, adresler vb talepler için dökümantasyon. src/app/docs/docs/docs.component.ts 63 @@ -8087,6 +8124,7 @@ Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance. + Electrum RPC için arayüz dökümantasyonu: Esplora'ya anında, kolayca ve emniyetli bir şekilde ulaşın. src/app/docs/docs/docs.component.ts 68 @@ -8403,6 +8441,7 @@ Overview for Lightning channel . See channel capacity, the Lightning nodes involved, related on-chain transactions, and more. + Lightning Kanalı için genel bakış sağlar. Kanal kapasitesi, bağlantılı Lightning nodeları, alakalı zincir üstü işlemler vb veriler. src/app/lightning/channel/channel-preview.component.ts 37 @@ -9030,6 +9069,7 @@ Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc). + Lightning Network için istatistikleri getir. ( toplam kapasite, bağlantılar vb), Ligthning nodeları (kanallar, likidite) ve Lightning kanalları (durum, ücretler vb) src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts 34 @@ -9139,6 +9179,7 @@ Overview for the Lightning network node named . See channels, capacity, location, fee stats, and more. + adındaki Lightning ağı nodu için genel bakış. Kanalları, kapasiteyi, lokasyonu, ücret bilgileri ve daha fazlasını gör. src/app/lightning/node/node-preview.component.ts 52 @@ -9338,6 +9379,7 @@ See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Tor-dışı Lightning ağı nodelarını dünya haritası üzerinde görselleştir. Haritadaki noktaların üzerinde gezerek node adı ve detayları görebilirsiniz. src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts 74 @@ -9362,6 +9404,7 @@ See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. + Tor-dışı Lightning ağı nodelarını dünya haritası üzerinde görselleştir. Haritadaki noktaların üzerinde gezerek node adı ve detayları görebilirsiniz. src/app/lightning/nodes-map/nodes-map.component.ts 52 @@ -9369,6 +9412,7 @@ See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both. + Ağ türüne göre Lightning ağı nodelarının zaman içerisindeki değişimini göster. Sadece clearnet (IPv4, IPv6), darknet (Tor, I2p, cjdns) ve iki tür bağlantı için. src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts 74 @@ -9437,6 +9481,7 @@ See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more. + Lightning network ağının coğrafi dağılımını görüntüle. Hangi ülkede kaç tane node bulunuyor, ülkeler için toplam BTC kapasitesi ve dha fazlası. src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts 47 @@ -9507,6 +9552,7 @@ Explore all the Lightning nodes hosted in and see an overview of each node's capacity, number of open channels, and more. + de çalıştırılan bütün Lightning nodeları içn node kapasitesi, açık node sayısı vb bilgileri incele. src/app/lightning/nodes-per-country/nodes-per-country.component.ts 44 @@ -9589,6 +9635,7 @@ Browse the top 100 ISPs hosting Lightning nodes along with stats like total number of nodes per ISP, aggregate BTC capacity per ISP, and more + En fazla Lightning Node'u barındıran 100 ISP'yi ve onların ISP başı toplam node sayısı, ISP'nin toplam BTC kapasitesi vb verilerini incele. src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts 54 @@ -9651,6 +9698,7 @@ Browse all Bitcoin Lightning nodes using the [AS] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP. + ISP [AS] kulanan bütün Lightning nodelarını ve onların toplam node sayısı, toplam kapasites vb görüntüle. src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts 45 @@ -9706,6 +9754,7 @@ See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc. + Lightning ağındaki en eski nodları ve bu nodeların kanal sayısı, kapasitesi ve lokasyonunu vb dataları görüntüle. src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts 28 @@ -9713,6 +9762,7 @@ See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more. + Lightning ağındaki en fazla BTC likiditesi olan nodelar için açık kanal sayısı, lokasyon, node yaşı vb dataları gör. src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts 35 @@ -9720,6 +9770,7 @@ See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more. + Lightning nodeları için toplam node kapasitesi, node yaşı vb temel dataları görüntüle. src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts 39 @@ -10093,6 +10144,7 @@ Your balance is too low.Please top up your account. + Balansınız çok düşük. lütfen hesabınıza ekleme yapınız . src/app/shared/components/mempool-error/mempool-error.component.html 9 From 26c03eee88101904e242b049153f6a47fccd8215 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 14 Aug 2024 14:21:47 +0000 Subject: [PATCH 11/73] update pool pie chart color scheme --- .../active-acceleration-box.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts index 7506fb6fc..f95bb71c8 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -67,13 +67,17 @@ export class ActiveAccelerationBox implements OnChanges { const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate); const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0); - const lightenStep = acceleratingPools.length ? (0.48 / acceleratingPools.length) : 0; + // Find the first pool with at least 1% of the total network hashrate + const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100); + const numSignificantPools = acceleratingPools.length - firstSignificantPool; acceleratingPools.forEach((poolId, index) => { const pool = pools[poolId]; const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); data.push(getDataItem( pool.lastEstimatedHashrate, - toRGB(lighten({ r: 147, g: 57, b: 244 }, index * lightenStep)), + index >= firstSignificantPool + ? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1))) + : 'white', `${pool.name} (${poolShare}%)`, true, ) as PieSeriesOption); From 248cef771869c226644ab3078ba768e7af789701 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 17 Aug 2024 00:14:33 +0000 Subject: [PATCH 12/73] Improve prioritized transaction detection algorithm --- backend/src/api/audit.ts | 22 +---- backend/src/api/transaction-utils.ts | 81 +++++++++++++++++++ .../block-overview-graph/tx-view.ts | 2 +- .../components/block-overview-graph/utils.ts | 4 + .../block-overview-tooltip.component.html | 5 ++ .../app/components/block/block.component.ts | 26 +++++- .../src/app/interfaces/node-api.interface.ts | 2 +- frontend/src/app/shared/transaction.utils.ts | 81 ++++++++++++++++++- 8 files changed, 201 insertions(+), 22 deletions(-) diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index eea96af69..e09234cdc 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -2,6 +2,7 @@ import config from '../config'; import logger from '../logger'; import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces'; import rbfCache from './rbf-cache'; +import transactionUtils from './transaction-utils'; const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners @@ -15,7 +16,8 @@ class Audit { const matches: string[] = []; // present in both mined block and template const added: string[] = []; // present in mined block, not in template const unseen: string[] = []; // present in the mined block, not in our mempool - const prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone + let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone + let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const accelerated: string[] = []; // prioritized by the mempool accelerator @@ -133,23 +135,7 @@ class Audit { totalWeight += tx.weight; } - - // identify "prioritized" transactions - let lastEffectiveRate = 0; - // Iterate over the mined template from bottom to top (excluding the coinbase) - // Transactions should appear in ascending order of mining priority. - for (let i = transactions.length - 1; i > 0; i--) { - const blockTx = transactions[i]; - // If a tx has a lower in-band effective fee rate than the previous tx, - // it must have been prioritized out-of-band (in order to have a higher mining priority) - // so exclude from the analysis. - if ((blockTx.effectiveFeePerVsize || 0) < lastEffectiveRate) { - prioritized.push(blockTx.txid); - // accelerated txs may or may not have their prioritized fee rate applied, so don't use them as a reference - } else if (!isAccelerated[blockTx.txid]) { - lastEffectiveRate = blockTx.effectiveFeePerVsize || 0; - } - } + ({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize')); // transactions missing from near the end of our template are probably not being censored let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight); diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index b3077b935..15d3e7110 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -338,6 +338,87 @@ class TransactionUtils { const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2; return witness[positionOfScript]; } + + // calculate the most parsimonious set of prioritizations given a list of block transactions + // (i.e. the most likely prioritizations and deprioritizations) + public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } { + // find the longest increasing subsequence of transactions + // (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms) + // should be O(n log n) + const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase) + if (X.length < 2) { + return { prioritized: [], deprioritized: [] }; + } + const N = X.length; + const P: number[] = new Array(N); + const M: number[] = new Array(N + 1); + M[0] = -1; // undefined so can be set to any value + + let L = 0; + for (let i = 0; i < N; i++) { + // Binary search for the smallest positive l ≤ L + // such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize + let lo = 1; + let hi = L + 1; + while (lo < hi) { + const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi + if (X[M[mid]].rate > X[i].rate) { + hi = mid; + } else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize + lo = mid + 1; + } + } + + // After searching, lo == hi is 1 greater than the + // length of the longest prefix of X[i] + const newL = lo; + + // The predecessor of X[i] is the last index of + // the subsequence of length newL-1 + P[i] = M[newL - 1]; + M[newL] = i; + + if (newL > L) { + // If we found a subsequence longer than any we've + // found yet, update L + L = newL; + } + } + + // Reconstruct the longest increasing subsequence + // It consists of the values of X at the L indices: + // ..., P[P[M[L]]], P[M[L]], M[L] + const LIS: any[] = new Array(L); + let k = M[L]; + for (let j = L - 1; j >= 0; j--) { + LIS[j] = X[k]; + k = P[k]; + } + + const lisMap = new Map(); + LIS.forEach((tx, index) => lisMap.set(tx.txid, index)); + + const prioritized: string[] = []; + const deprioritized: string[] = []; + + let lastRate = X[0].rate; + + for (const tx of X) { + if (lisMap.has(tx.txid)) { + lastRate = tx.rate; + } else { + if (Math.abs(tx.rate - lastRate) < 0.1) { + // skip if the rate is almost the same as the previous transaction + } else if (tx.rate <= lastRate) { + prioritized.push(tx.txid); + } else { + deprioritized.push(tx.txid); + } + } + } + + return { prioritized, deprioritized }; + } } export default new TransactionUtils(); diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index ad24b26c3..f612368f4 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -33,7 +33,7 @@ export default class TxView implements TransactionStripped { flags: number; bigintFlags?: bigint | null = 0b00000100_00000000_00000000_00000000n; time?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; scene?: BlockScene; diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index 4f7c7ed5a..625029db0 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -142,6 +142,10 @@ export function defaultColorFunction( return auditColors.added_prioritized; case 'prioritized': return auditColors.prioritized; + case 'added_deprioritized': + return auditColors.added_prioritized; + case 'deprioritized': + return auditColors.prioritized; case 'selected': return colors.marginal[levelIndex] || colors.marginal[defaultMempoolFeeColors.length - 1]; case 'accelerated': diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index 037229398..f1f5bb3d4 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -79,6 +79,11 @@ Added Prioritized + Deprioritized + + Added + Deprioritized + Marginal fee rate Conflict Accelerated diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 44328c591..5cba85e90 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -17,6 +17,7 @@ import { PriceService, Price } from '../../services/price.service'; import { CacheService } from '../../services/cache.service'; import { ServicesApiServices } from '../../services/services-api.service'; import { PreloadService } from '../../services/preload.service'; +import { identifyPrioritizedTransactions } from '../../shared/transaction.utils'; @Component({ selector: 'app-block', @@ -524,6 +525,7 @@ export class BlockComponent implements OnInit, OnDestroy { const isUnseen = {}; const isAdded = {}; const isPrioritized = {}; + const isDeprioritized = {}; const isCensored = {}; const isMissing = {}; const isSelected = {}; @@ -535,6 +537,17 @@ export class BlockComponent implements OnInit, OnDestroy { this.numUnexpected = 0; if (blockAudit?.template) { + // augment with locally calculated *de*prioritized transactions if possible + const { prioritized, deprioritized } = identifyPrioritizedTransactions(transactions); + // but if the local calculation produces returns unexpected results, don't use it + let useLocalDeprioritized = deprioritized.length < (transactions.length * 0.1); + for (const tx of prioritized) { + if (!isPrioritized[tx] && !isAccelerated[tx]) { + useLocalDeprioritized = false; + break; + } + } + for (const tx of blockAudit.template) { inTemplate[tx.txid] = true; if (tx.acc) { @@ -550,9 +563,14 @@ export class BlockComponent implements OnInit, OnDestroy { for (const txid of blockAudit.addedTxs) { isAdded[txid] = true; } - for (const txid of blockAudit.prioritizedTxs || []) { + for (const txid of blockAudit.prioritizedTxs) { isPrioritized[txid] = true; } + if (useLocalDeprioritized) { + for (const txid of deprioritized || []) { + isDeprioritized[txid] = true; + } + } for (const txid of blockAudit.missingTxs) { isCensored[txid] = true; } @@ -608,6 +626,12 @@ export class BlockComponent implements OnInit, OnDestroy { } else { tx.status = 'prioritized'; } + } else if (isDeprioritized[tx.txid]) { + if (isAdded[tx.txid] || (blockAudit.version > 0 && isUnseen[tx.txid])) { + tx.status = 'added_deprioritized'; + } else { + tx.status = 'deprioritized'; + } } else if (isAdded[tx.txid] && (blockAudit.version === 0 || isUnseen[tx.txid])) { tx.status = 'added'; } else if (inTemplate[tx.txid]) { diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 4d2ffc09a..3e38ff88b 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -239,7 +239,7 @@ export interface TransactionStripped { acc?: boolean; flags?: number | null; time?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'added_prioritized' | 'prioritized' | 'added_deprioritized' | 'deprioritized' | 'censored' | 'selected' | 'rbf' | 'accelerated'; context?: 'projected' | 'actual'; } diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index 9d9cd801b..c13616c60 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -1,7 +1,7 @@ import { TransactionFlags } from './filters.utils'; import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; import { Transaction } from '../interfaces/electrs.interface'; -import { CpfpInfo, RbfInfo } from '../interfaces/node-api.interface'; +import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface'; // Bitcoin Core default policy settings const TX_MAX_STANDARD_VERSION = 2; @@ -458,4 +458,83 @@ export function getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): } else { return tx.effectiveFeePerVsize; } +} + +export function identifyPrioritizedTransactions(transactions: TransactionStripped[]): { prioritized: string[], deprioritized: string[] } { + // find the longest increasing subsequence of transactions + // (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms) + // should be O(n log n) + const X = transactions.slice(1).reverse(); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase) + if (X.length < 2) { + return { prioritized: [], deprioritized: [] }; + } + const N = X.length; + const P: number[] = new Array(N); + const M: number[] = new Array(N + 1); + M[0] = -1; // undefined so can be set to any value + + let L = 0; + for (let i = 0; i < N; i++) { + // Binary search for the smallest positive l ≤ L + // such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize + let lo = 1; + let hi = L + 1; + while (lo < hi) { + const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi + if (X[M[mid]].rate > X[i].rate) { + hi = mid; + } else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize + lo = mid + 1; + } + } + + // After searching, lo == hi is 1 greater than the + // length of the longest prefix of X[i] + const newL = lo; + + // The predecessor of X[i] is the last index of + // the subsequence of length newL-1 + P[i] = M[newL - 1]; + M[newL] = i; + + if (newL > L) { + // If we found a subsequence longer than any we've + // found yet, update L + L = newL; + } + } + + // Reconstruct the longest increasing subsequence + // It consists of the values of X at the L indices: + // ..., P[P[M[L]]], P[M[L]], M[L] + const LIS: TransactionStripped[] = new Array(L); + let k = M[L]; + for (let j = L - 1; j >= 0; j--) { + LIS[j] = X[k]; + k = P[k]; + } + + const lisMap = new Map(); + LIS.forEach((tx, index) => lisMap.set(tx.txid, index)); + + const prioritized: string[] = []; + const deprioritized: string[] = []; + + let lastRate = 0; + + for (const tx of X) { + if (lisMap.has(tx.txid)) { + lastRate = tx.rate; + } else { + if (Math.abs(tx.rate - lastRate) < 0.1) { + // skip if the rate is almost the same as the previous transaction + } else if (tx.rate <= lastRate) { + prioritized.push(tx.txid); + } else { + deprioritized.push(tx.txid); + } + } + } + + return { prioritized, deprioritized }; } \ No newline at end of file From c9171224e145852dac897a69b16ecc4d6a97ef23 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 17 Aug 2024 01:09:31 +0000 Subject: [PATCH 13/73] DB migration to fix bad v1 audits --- backend/src/api/database-migration.ts | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 6ddca7697..95f8c8707 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository'; import { RowDataPacket } from 'mysql2'; class DatabaseMigration { - private static currentVersion = 81; + private static currentVersion = 82; private queryTimeout = 3600_000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -700,6 +700,11 @@ class DatabaseMigration { await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"'); await this.updateToSchemaVersion(81); } + + if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') { + await this.$fixBadV1AuditBlocks(); + await this.updateToSchemaVersion(82); + } } /** @@ -1314,6 +1319,28 @@ class DatabaseMigration { logger.warn(`Failed to migrate cpfp transaction data`); } } + + private async $fixBadV1AuditBlocks(): Promise { + const badBlocks = [ + '000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc', + '000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960', + '000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7', + '00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286', + '0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb', + ]; + + for (const hash of badBlocks) { + try { + await this.$executeQuery(` + UPDATE blocks_audits + SET prioritized_txs = '[]' + WHERE hash = '${hash}' + `, true); + } catch (e) { + continue; + } + } + } } export default new DatabaseMigration(); From e3c4e219f31f9678119827a2be56f7dc655f02ce Mon Sep 17 00:00:00 2001 From: natsoni Date: Sun, 18 Aug 2024 14:15:56 +0200 Subject: [PATCH 14/73] Fix accelerated arrow not appearing --- .../app/components/mempool-blocks/mempool-blocks.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index a0958ec40..af5a91c65 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -213,7 +213,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { } if (state.mempoolPosition) { this.txPosition = state.mempoolPosition; - if (this.txPosition.accelerated && !oldTxPosition.accelerated) { + if (this.txPosition.accelerated && !oldTxPosition?.accelerated) { this.acceleratingArrow = true; setTimeout(() => { this.acceleratingArrow = false; From b3ac107b0b5bd31ebe1f0957a8730f7081229bdf Mon Sep 17 00:00:00 2001 From: natsoni Date: Sun, 18 Aug 2024 18:33:25 +0200 Subject: [PATCH 15/73] clear feeDelta if a tx is mined by non-participating pool --- .../transaction/transaction.component.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 637aa52e3..8c0d3b4a9 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -359,12 +359,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { ).subscribe((accelerationHistory) => { for (const acceleration of accelerationHistory) { if (acceleration.txid === this.txId) { - if ((acceleration.status === 'completed' || acceleration.status === 'completed_provisional') && acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { - const boostCost = acceleration.boostCost || acceleration.bidBoost; - acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; - acceleration.boost = boostCost; - this.tx.acceleratedAt = acceleration.added; - this.accelerationInfo = acceleration; + if (acceleration.status === 'completed' || acceleration.status === 'completed_provisional') { + if (acceleration.pools.includes(acceleration.minedByPoolUniqueId)) { + const boostCost = acceleration.boostCost || acceleration.bidBoost; + acceleration.acceleratedFeeRate = Math.max(acceleration.effectiveFee, acceleration.effectiveFee + boostCost) / acceleration.effectiveVsize; + acceleration.boost = boostCost; + this.tx.acceleratedAt = acceleration.added; + this.accelerationInfo = acceleration; + } else { + this.tx.feeDelta = undefined; + } } this.waitingForAccelerationInfo = false; this.setIsAccelerated(); From f75f85f914e75d20fd978472a12593fe360d5924 Mon Sep 17 00:00:00 2001 From: natsoni Date: Sun, 18 Aug 2024 19:43:38 +0200 Subject: [PATCH 16/73] Hide fee delta on accelerated tx mined by participating pool with 0 bid boost --- .../transaction/transaction.component.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 2ae6c8df8..715fca4c8 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -607,14 +607,10 @@ Fee {{ tx.fee | number }} sat - - @if (accelerationInfo?.bidBoost) { - +{{ accelerationInfo.bidBoost | number }} sat - } @else if (tx.feeDelta) { - +{{ tx.feeDelta | number }} sat - } - - + @if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { + +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sat + } + } @else { From 80da024bbb787bca5e62fdc51e5b30d20d1ed2f4 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 19 Aug 2024 14:49:36 +0900 Subject: [PATCH 17/73] Add hr locale to angular.json --- frontend/angular.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/angular.json b/frontend/angular.json index 190982225..3aa1cb6a8 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -54,6 +54,10 @@ "translation": "src/locale/messages.fr.xlf", "baseHref": "/fr/" }, + "hr": { + "translation": "src/locale/messages.hr.xlf", + "baseHref": "/hr/" + }, "ja": { "translation": "src/locale/messages.ja.xlf", "baseHref": "/ja/" From c7f48b4390a7973bb97b02b6cfe239d0b5feee8c Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 19 Aug 2024 16:29:34 +0200 Subject: [PATCH 18/73] Add amount mode selector to footer --- .../amount-selector.component.html | 7 ++++ .../amount-selector.component.scss | 0 .../amount-selector.component.ts | 36 +++++++++++++++++++ .../global-footer.component.html | 6 +++- .../global-footer.component.scss | 5 +++ frontend/src/app/shared/shared.module.ts | 3 ++ 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.html create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.scss create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.ts diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.html b/frontend/src/app/components/amount-selector/amount-selector.component.html new file mode 100644 index 000000000..b509d6fe3 --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.html @@ -0,0 +1,7 @@ +

+ +
diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.scss b/frontend/src/app/components/amount-selector/amount-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.ts b/frontend/src/app/components/amount-selector/amount-selector.component.ts new file mode 100644 index 000000000..144b0f1db --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { StorageService } from '../../services/storage.service'; +import { StateService } from '../../services/state.service'; + +@Component({ + selector: 'app-amount-selector', + templateUrl: './amount-selector.component.html', + styleUrls: ['./amount-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AmountSelectorComponent implements OnInit { + amountForm: UntypedFormGroup; + modes = ['btc', 'sats', 'fiat']; + + constructor( + private formBuilder: UntypedFormBuilder, + private stateService: StateService, + private storageService: StorageService, + ) { } + + ngOnInit() { + this.amountForm = this.formBuilder.group({ + mode: ['btc'] + }); + this.stateService.viewAmountMode$.subscribe((mode) => { + this.amountForm.get('mode')?.setValue(mode); + }); + } + + changeMode() { + const newMode = this.amountForm.get('mode')?.value; + this.storageService.setValue('view-amount-mode', newMode); + this.stateService.viewAmountMode$.next(newMode); + } +} diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index a2e7286e0..1765bc6fc 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -27,6 +27,9 @@
+
+ +
@if (!env.customize?.theme) {
@@ -39,7 +42,8 @@
@if (!env.customize?.theme) {
- + +
} @if (!enterpriseInfo?.footer_img) { diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index e0daf4f4c..b815da754 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -76,6 +76,11 @@ footer .selector { display: inline-block; } +footer .add-margin { + margin-left: 5px; + margin-right: 5px; +} + footer .row.link-tree { max-width: 1140px; margin: 0 auto; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2d5b4d0f9..2e300a300 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -35,6 +35,7 @@ import { LanguageSelectorComponent } from '../components/language-selector/langu import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component'; import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component'; import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component'; +import { AmountSelectorComponent } from '../components/amount-selector/amount-selector.component'; import { BrowserOnlyDirective } from './directives/browser-only.directive'; import { ServerOnlyDirective } from './directives/server-only.directive'; import { ColoredPriceDirective } from './directives/colored-price.directive'; @@ -131,6 +132,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, ThemeSelectorComponent, RateUnitSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, NoSanitizePipe, @@ -278,6 +280,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, RateUnitSelectorComponent, ThemeSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, Hex2asciiPipe, From e59308c2f5ab79d24e9a3f2fca7ecb135284fcac Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 19 Aug 2024 17:13:41 +0200 Subject: [PATCH 19/73] Fix global footer css --- .../global-footer.component.html | 10 ++++---- .../global-footer.component.scss | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 1765bc6fc..fbc2c89eb 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -27,27 +27,27 @@
-
+
@if (!env.customize?.theme) { - @if (!env.customize?.theme) { -
+
} @if (!enterpriseInfo?.footer_img) { - diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index b815da754..bf47d5489 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -159,7 +159,7 @@ footer .nowrap { display: block; } -@media (min-width: 951px) { +@media (min-width: 1020px) { :host-context(.ltr-layout) .language-selector { float: right !important; } @@ -177,7 +177,24 @@ footer .nowrap { } .services { - @media (min-width: 951px) and (max-width: 1147px) { + @media (min-width: 1300px) { + :host-context(.ltr-layout) .language-selector { + float: right !important; + } + :host-context(.rtl-layout) .language-selector { + float: left !important; + } + + .explore-tagline-desktop { + display: block; + } + + .explore-tagline-mobile { + display: none; + } + } + + @media (max-width: 1300px) { :host-context(.ltr-layout) .services .language-selector { float: none !important; } @@ -253,7 +270,7 @@ footer .nowrap { } -@media (max-width: 950px) { +@media (max-width: 1019px) { .main-logo { width: 220px; @@ -292,7 +309,7 @@ footer .nowrap { } } -@media (max-width: 1147px) { +@media (max-width: 1300px) { .services.main-logo { width: 220px; From 9572f2d554c2a824c468517e92bedabc5261c708 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Mon, 19 Aug 2024 20:13:49 +0200 Subject: [PATCH 20/73] Add logo images and references to logos --- LICENSE | 12 +++++++----- .../app/components/about/about.component.html | 2 +- .../trademark-policy.component.html | 17 ++++++++++++++++- .../resources/mempool-block-visualization.png | Bin 0 -> 15888 bytes frontend/src/resources/mempool-research.png | Bin 0 -> 52995 bytes frontend/src/resources/mempool-transaction.png | Bin 0 -> 61277 bytes 6 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 frontend/src/resources/mempool-block-visualization.png create mode 100644 frontend/src/resources/mempool-research.png create mode 100644 frontend/src/resources/mempool-transaction.png diff --git a/LICENSE b/LICENSE index b6a09390a..1c368c00a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The Mempool Open Source Project® -Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders +Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free @@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project. The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full -Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo, -the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical -Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks -of Mempool Space K.K in Japan, the United States, and/or other countries. +Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo, +the mempool block visualization Logo, the mempool Blocks Logo, the mempool +transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, +the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are +registered trademarks or trademarks of Mempool Space K.K in Japan, +the United States, and/or other countries. See our full Trademark Policy and Guidelines for more details, published on . diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 41c0ce47f..e04edf226 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -435,7 +435,7 @@ Trademark Notice

- The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries. + The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.

While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our Trademark Policy and Guidelines for more details, published on <https://mempool.space/trademark-policy>. diff --git a/frontend/src/app/components/trademark-policy/trademark-policy.component.html b/frontend/src/app/components/trademark-policy/trademark-policy.component.html index de1d78daa..0a0dde251 100644 --- a/frontend/src/app/components/trademark-policy/trademark-policy.component.html +++ b/frontend/src/app/components/trademark-policy/trademark-policy.component.html @@ -8,7 +8,7 @@

Trademark Policy and Guidelines

The Mempool Open Source Project ®
-
Updated: July 3, 2024
+
Updated: August 19, 2024

@@ -100,11 +100,26 @@

The Mempool Accelerator Logo



+ +

+

The mempool research Logo

+

+

The Mempool Goggles Logo



+ +

+

The mempool transaction Logo

+

+ + +

+

The mempool block visualization Logo

+

+

The mempool Blocks Logo

diff --git a/frontend/src/resources/mempool-block-visualization.png b/frontend/src/resources/mempool-block-visualization.png new file mode 100644 index 0000000000000000000000000000000000000000..7b808a69a400c530a247511090dad47fbe38f0be GIT binary patch literal 15888 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 v^y=cIna@Ng{rE!9+$JbT+fD8al<1 literal 0 HcmV?d00001 diff --git a/frontend/src/resources/mempool-research.png b/frontend/src/resources/mempool-research.png new file mode 100644 index 0000000000000000000000000000000000000000..42ee008f74a04ed7334f54b2694c8067db98b702 GIT binary patch literal 52995 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 QbKh>*T02_Ro%=K4UjbJ1EdT%j literal 0 HcmV?d00001 diff --git a/frontend/src/resources/mempool-transaction.png b/frontend/src/resources/mempool-transaction.png new file mode 100644 index 0000000000000000000000000000000000000000..396f476336aca852bc073515b1b3235120da76c7 GIT binary patch literal 61277 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 Xetzt0YdbQ=K;MjDrh0Fow;uin!{X3F literal 0 HcmV?d00001 From b78aca0282baf4d2ed18ca08f7f0d345d8847b08 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 19 Aug 2024 19:46:22 +0000 Subject: [PATCH 21/73] Fix about page layout --- frontend/src/app/components/about/about.component.html | 8 +++++--- frontend/src/app/components/about/about.component.scss | 7 ++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 41c0ce47f..5b8a69e62 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -53,7 +53,7 @@ Spiral - + Blockstream - + + + Unchained @@ -150,7 +152,7 @@ Bull Bitcoin - + diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index a360e180c..41e9209b7 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -13,8 +13,6 @@ .image.not-rounded { border-radius: 0; - width: 60px; - height: 60px; } .intro { @@ -158,9 +156,8 @@ margin: 40px 29px 10px; &.image.coldcard { border-radius: 0; - width: auto; - max-height: 50px; - margin: 40px 29px 14px 29px; + height: auto; + margin: 20px 29px 20px; } } } From 4e581347c851b62f359e201f61b303f28ea42580 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 20 Aug 2024 12:06:11 +0900 Subject: [PATCH 22/73] Bump version to v3.0.0-rc1 --- backend/package-lock.json | 4 ++-- backend/package.json | 2 +- frontend/cypress/fixtures/mainnet_mempoolInfo.json | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- unfurler/package-lock.json | 4 ++-- unfurler/package.json | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 944abfdb2..66e8d19a2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-backend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { diff --git a/backend/package.json b/backend/package.json index a5fb4bdbe..959516ac8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/frontend/cypress/fixtures/mainnet_mempoolInfo.json b/frontend/cypress/fixtures/mainnet_mempoolInfo.json index e0b0fa6b9..d9e441277 100644 --- a/frontend/cypress/fixtures/mainnet_mempoolInfo.json +++ b/frontend/cypress/fixtures/mainnet_mempoolInfo.json @@ -750,7 +750,7 @@ }, "backendInfo": { "hostname": "node205.tk7.mempool.space", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "gitCommit": "abbc8a134", "lightning": false }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 37917526c..a75c49bf3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", diff --git a/frontend/package.json b/frontend/package.json index c810cac00..b9fa4d3bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json index 92b9d307b..8c6e77883 100644 --- a/unfurler/package-lock.json +++ b/unfurler/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-unfurl", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-unfurl", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "dependencies": { "@types/node": "^16.11.41", "ejs": "^3.1.10", diff --git a/unfurler/package.json b/unfurler/package.json index 8a87e88d8..c0d372e6f 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -1,6 +1,6 @@ { "name": "mempool-unfurl", - "version": "3.0.0-beta", + "version": "3.0.0-rc1", "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", From ff9e2456b9340d144960670ce5aec2cb39c3a408 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 20 Aug 2024 15:22:16 +0900 Subject: [PATCH 23/73] ops: Tweak build script to support tags --- production/mempool-build-all | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/mempool-build-all b/production/mempool-build-all index 601f15b9a..84ea1b5ec 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -40,7 +40,7 @@ update_repo() git fetch origin || exit 1 for remote in origin;do git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1 - git fetch "${remote}" || exit 1 + git fetch "${remote}" --tags || exit 1 done if [ $(git tag -l "${REF}") ];then From 5452d7f52465a39c2fd2809dcc78dfa1a80ee4f0 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 20 Aug 2024 09:32:56 +0200 Subject: [PATCH 24/73] pull from transifex --- frontend/src/locale/messages.hr.xlf | 22 +++++++++++----------- frontend/src/locale/messages.nb.xlf | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/locale/messages.hr.xlf b/frontend/src/locale/messages.hr.xlf index e0f2ceb89..d013c70bd 100644 --- a/frontend/src/locale/messages.hr.xlf +++ b/frontend/src/locale/messages.hr.xlf @@ -1567,7 +1567,7 @@ Total Bid Boost - Ukupno povećanje ponude + Total Bid Boost src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html 11 @@ -1969,7 +1969,7 @@ Avg Max Bid - Prosječna maks. ponuda + Prosj. max ponuda src/app/components/acceleration/pending-stats/pending-stats.component.html 11 @@ -3622,7 +3622,7 @@ transactions - transakcije + transakcija src/app/components/block/block-transactions.component.html 5 @@ -4046,7 +4046,7 @@ Blocks - Blokovi + Blokova src/app/components/blocks-list/blocks-list.component.html 4 @@ -4360,7 +4360,7 @@ Previous fee - Prethodna naknada + Preth. naknada src/app/components/custom-dashboard/custom-dashboard.component.html 107 @@ -4594,7 +4594,7 @@ blocks - blokovi + blokova src/app/components/difficulty-mining/difficulty-mining.component.html 10,11 @@ -4671,7 +4671,7 @@ Next Halving - Sljedeće prepolovljenje + Slj prepolovljenje src/app/components/difficulty-mining/difficulty-mining.component.html 47 @@ -5465,7 +5465,7 @@ blocks - blokovi + blokova src/app/components/liquid-reserves-audit/federation-utxos-list/federation-utxos-list.component.html 63 @@ -5885,7 +5885,7 @@ Blocks (1w) - Blokova (1w) + Blokova (1 tj) src/app/components/pool-ranking/pool-ranking.component.html 25 @@ -6165,7 +6165,7 @@ Blocks (24h) - Blokovi (24h) + Blokova (24h) src/app/components/pool/pool.component.html 120 @@ -6815,7 +6815,7 @@ In ~ - U ~ + Za ~ src/app/components/time/time.component.ts 188 diff --git a/frontend/src/locale/messages.nb.xlf b/frontend/src/locale/messages.nb.xlf index 5fbc688c4..b2de6219c 100644 --- a/frontend/src/locale/messages.nb.xlf +++ b/frontend/src/locale/messages.nb.xlf @@ -510,7 +510,7 @@ sats - sats + sat src/app/components/accelerate-checkout/accelerate-checkout.component.html 57 @@ -881,7 +881,7 @@ Pay - Betale + Betal src/app/components/accelerate-checkout/accelerate-checkout.component.html 378 @@ -4846,7 +4846,7 @@ Amount (sats) - Beløp (sats) + Beløp (sat) src/app/components/faucet/faucet.component.html 51 @@ -6442,7 +6442,7 @@ sats/tx - sats/tx + sat/tx src/app/components/reward-stats/reward-stats.component.html 33 @@ -8145,7 +8145,7 @@ mSats - mSats + mSat src/app/lightning/channel/channel-box/channel-box.component.html 35 From d22743c4b81331768d4e44cab5f4a0d213743434 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 22 Aug 2024 15:39:20 +0200 Subject: [PATCH 25/73] Don't display accelerator checkout on already accelerated txs --- .../app/components/transaction/transaction.component.html | 8 ++++---- .../app/components/transaction/transaction.component.ts | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 715fca4c8..553d3221f 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -552,18 +552,18 @@ @if (eta.blocks >= 7) { - + Not any time soon - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration && notAcceleratedOnLoad) { Accelerate } } @else if (network === 'liquid' || network === 'liquidtestnet') { } @else { - + - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration && notAcceleratedOnLoad) { Accelerate } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..4d0818c72 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { firstLoad = true; waitingForAccelerationInfo: boolean = false; isLoadingFirstSeen = false; + notAcceleratedOnLoad: boolean = null; featuresEnabled: boolean; segwitEnabled: boolean; @@ -848,6 +849,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.tx.feeDelta = cpfpInfo.feeDelta; this.setIsAccelerated(firstCpfp); } + + if (this.notAcceleratedOnLoad === null) { + this.notAcceleratedOnLoad = !this.isAcceleration; + } if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { this.forceAccelerationSummary = true; @@ -1083,6 +1088,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { (!this.hideAccelerationSummary && !this.accelerationFlowCompleted) || this.forceAccelerationSummary ) + && this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs ); } From b47e1486775a35593a8b9233cdee3c700fe463b3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 22 Aug 2024 19:51:28 +0000 Subject: [PATCH 26/73] respect json Accept header in API error responses --- backend/src/api/bitcoin/bitcoin.routes.ts | 144 +++++++++++--------- backend/src/api/explorer/channels.routes.ts | 19 +-- backend/src/api/explorer/general.routes.ts | 8 +- backend/src/api/explorer/nodes.routes.ts | 39 +++--- backend/src/api/liquid/liquid.routes.ts | 35 ++--- backend/src/api/mining/mining-routes.ts | 73 +++++----- backend/src/utils/api.ts | 9 ++ 7 files changed, 177 insertions(+), 150 deletions(-) create mode 100644 backend/src/utils/api.ts diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 6225a9c1d..498003d98 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; +import { handleError } from '../../utils/api'; class BitcoinRoutes { public initRoutes(app: Application) { @@ -86,7 +87,7 @@ class BitcoinRoutes { res.set('Content-Type', 'application/json'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -105,13 +106,13 @@ class BitcoinRoutes { const result = mempoolBlocks.getMempoolBlocks(); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private getTransactionTimes(req: Request, res: Response) { if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); + handleError(req, res, 500, 'Not an array'); return; } const txIds: string[] = []; @@ -128,12 +129,12 @@ class BitcoinRoutes { private async $getBatchedOutspends(req: Request, res: Response): Promise { const txids_csv = req.query.txids; if (!txids_csv || typeof txids_csv !== 'string') { - res.status(500).send('Invalid txids format'); + handleError(req, res, 500, 'Invalid txids format'); return; } const txids = txids_csv.split(','); if (txids.length > 50) { - res.status(400).send('Too many txids requested'); + handleError(req, res, 400, 'Too many txids requested'); return; } @@ -141,13 +142,13 @@ class BitcoinRoutes { const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); res.json(batchedOutspends); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getCpfpInfo(req: Request, res: Response) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - res.status(501).send(`Invalid transaction ID.`); + handleError(req, res, 501, `Invalid transaction ID.`); return; } @@ -180,7 +181,7 @@ class BitcoinRoutes { try { cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); } catch (e) { - res.status(500).send('failed to get CPFP info'); + handleError(req, res, 500, 'failed to get CPFP info'); return; } } @@ -209,7 +210,7 @@ class BitcoinRoutes { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -223,7 +224,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -284,13 +285,13 @@ class BitcoinRoutes { // Not modified // 422 Unprocessable Entity // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 - res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); + handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`); } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -304,7 +305,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -314,7 +315,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -336,7 +337,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -346,7 +347,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(blockHeader); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -357,10 +358,11 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`audit not available`); + handleError(req, res, 404, `audit not available`); + return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +373,8 @@ class BitcoinRoutes { 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`); + handleError(req, res, 404, `transaction audit not available`); + return; } } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); @@ -388,42 +391,49 @@ class BitcoinRoutes { return await this.getLegacyBlocks(req, res); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getBlocksByBulk(req: Request, res: Response) { try { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented - return res.status(404).send(`This API is only available for Bitcoin networks`); + handleError(req, res, 404, `This API is only available for Bitcoin networks`); + return; } if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { - return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + return; } if (!Common.indexingEnabled()) { - return res.status(404).send(`Indexing is required for this API`); + handleError(req, res, 404, `Indexing is required for this API`); + return; } const from = parseInt(req.params.from, 10); if (!req.params.from || from < 0) { - return res.status(400).send(`Parameter 'from' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); + return; } const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); if (to < 0) { - return res.status(400).send(`Parameter 'to' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); + return; } if (from > to) { - return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); + handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); + return; } if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { - return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + return; } res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await blocks.$getBlocksBetweenHeight(from, to)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -458,10 +468,10 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(returnBlocks); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } - + private async getBlockTransactions(req: Request, res: Response) { try { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); @@ -483,7 +493,7 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -492,13 +502,13 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddress(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -507,15 +517,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -528,23 +539,23 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Address summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.'); return; } } private async getScriptHash(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -555,15 +566,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -578,16 +590,16 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.'); return; } } @@ -597,7 +609,7 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -624,7 +636,7 @@ class BitcoinRoutes { const rawMempool = await bitcoinApi.$getRawMempool(); res.send(rawMempool); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -632,12 +644,13 @@ class BitcoinRoutes { try { const result = blocks.getCurrentBlockHeight(); if (!result) { - return res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; } res.setHeader('content-type', 'text/plain'); res.send(result.toString()); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -647,7 +660,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -657,7 +670,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -666,7 +679,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -675,7 +688,7 @@ class BitcoinRoutes { const result = await bitcoinClient.validateAddress(req.params.address); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -688,7 +701,7 @@ class BitcoinRoutes { replaces }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -697,7 +710,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(false); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -706,7 +719,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(true); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -719,7 +732,7 @@ class BitcoinRoutes { res.status(204).send(); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -728,7 +741,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getOutspends(req.params.txId); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -738,10 +751,10 @@ class BitcoinRoutes { if (da) { res.json(da); } else { - res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -752,7 +765,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -764,7 +777,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinClient.sendRawTransaction(txHex); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -776,8 +789,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { - res.setHeader('content-type', 'text/plain'); - res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 391bf628e..8b4c3e8c8 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -1,6 +1,7 @@ import config from '../../config'; import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; +import { handleError } from '../../utils/api'; class ChannelsRoutes { constructor() { } @@ -22,7 +23,7 @@ class ChannelsRoutes { const channels = await channelsApi.$searchChannelsById(req.params.search); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -38,7 +39,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channel); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -53,11 +54,11 @@ class ChannelsRoutes { const status: string = typeof req.query.status === 'string' ? req.query.status : ''; if (index < -1) { - res.status(400).send('Invalid index'); + handleError(req, res, 400, 'Invalid index'); return; } if (['open', 'active', 'closed'].includes(status) === false) { - res.status(400).send('Invalid status'); + handleError(req, res, 400, 'Invalid status'); return; } @@ -69,14 +70,14 @@ class ChannelsRoutes { res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getChannelsByTransactionIds(req: Request, res: Response): Promise { try { if (!Array.isArray(req.query.txId)) { - res.status(400).send('Not an array'); + handleError(req, res, 400, 'Not an array'); return; } const txIds: string[] = []; @@ -107,7 +108,7 @@ class ChannelsRoutes { res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -119,7 +120,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -132,7 +133,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts index 07620e84a..b4d0c635d 100644 --- a/backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; import statisticsApi from './statistics.api'; +import { handleError } from '../../utils/api'; + class GeneralLightningRoutes { constructor() { } @@ -27,7 +29,7 @@ class GeneralLightningRoutes { channels: channels, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -41,7 +43,7 @@ class GeneralLightningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -50,7 +52,7 @@ class GeneralLightningRoutes { const statistics = await statisticsApi.$getLatestStatistics(); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 9d6373845..9ca2fd1c3 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import DB from '../../database'; import { INodesRanking } from '../../mempool.interfaces'; +import { handleError } from '../../utils/api'; class NodesRoutes { constructor() { } @@ -31,7 +32,7 @@ class NodesRoutes { const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -181,13 +182,13 @@ class NodesRoutes { } } catch (e) {} } - + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -195,7 +196,7 @@ class NodesRoutes { try { const node = await nodesApi.$getNode(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -203,7 +204,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -215,7 +216,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -223,7 +224,7 @@ class NodesRoutes { try { const node = await nodesApi.$getFeeHistogram(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -231,7 +232,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -247,7 +248,7 @@ class NodesRoutes { topByChannels: topChannelsNodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -259,7 +260,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -271,7 +272,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -283,7 +284,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -295,7 +296,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -307,7 +308,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(worldNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -322,7 +323,7 @@ class NodesRoutes { ); if (country.length === 0) { - res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`); return; } @@ -335,7 +336,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -349,7 +350,7 @@ class NodesRoutes { ); if (isp.length === 0) { - res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`); return; } @@ -362,7 +363,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -374,7 +375,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 9ea61ca31..9dafd0def 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import elementsParser from './elements-parser'; import icons from './icons'; +import { handleError } from '../../utils/api'; class LiquidRoutes { public initRoutes(app: Application) { @@ -42,7 +43,7 @@ class LiquidRoutes { res.setHeader('content-length', result.length); res.send(result); } else { - res.status(404).send('Asset icon not found'); + handleError(req, res, 404, 'Asset icon not found'); } } @@ -51,7 +52,7 @@ class LiquidRoutes { if (result) { res.json(result); } else { - res.status(404).send('Asset icons not found'); + handleError(req, res, 404, 'Asset icons not found'); } } @@ -82,7 +83,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -94,7 +95,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(reserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -106,7 +107,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentSupply); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -118,7 +119,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentReserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -130,7 +131,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(auditStatus); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -142,7 +143,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -154,7 +155,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -166,7 +167,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -178,7 +179,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(expiredUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -190,7 +191,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -202,7 +203,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -214,7 +215,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -226,7 +227,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(recentPegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -238,7 +239,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsVolume); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -250,7 +251,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsCount); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 8f8bbac82..69e6d95d4 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -10,6 +10,7 @@ import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; import accelerationApi from '../services/acceleration'; +import { handleError } from '../../utils/api'; class MiningRoutes { public initRoutes(app: Application) { @@ -53,12 +54,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Prices are not available on testnets.'); + handleError(req, res, 400, 'Prices are not available on testnets.'); return; } const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const currency = req.query.currency as string; - + let response; if (timestamp && currency) { response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); @@ -71,7 +72,7 @@ class MiningRoutes { } res.status(200).send(response); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -84,9 +85,9 @@ class MiningRoutes { res.json(stats); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -103,9 +104,9 @@ class MiningRoutes { res.json(poolBlocks); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -129,7 +130,7 @@ class MiningRoutes { res.json(pools); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -143,7 +144,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -157,7 +158,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(hashrates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -172,9 +173,9 @@ class MiningRoutes { res.json(hashrates); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -203,7 +204,7 @@ class MiningRoutes { currentDifficulty: currentDifficulty, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -217,7 +218,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -235,7 +236,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -249,7 +250,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockRewards); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -263,7 +264,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFeeRates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -281,7 +282,7 @@ class MiningRoutes { weights: blockWeights }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -293,7 +294,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -317,7 +318,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -326,7 +327,7 @@ class MiningRoutes { const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); if (!audit) { - res.status(204).send(`This block has not been audited.`); + handleError(req, res, 204, `This block has not been audited.`); return; } @@ -335,7 +336,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -358,7 +359,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +372,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -384,7 +385,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit || 'null'); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -394,12 +395,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -409,13 +410,13 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -425,12 +426,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -440,12 +441,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationTotals(req.query.pool, req.query.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -455,12 +456,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(accelerationApi.accelerations || []); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -472,7 +473,7 @@ class MiningRoutes { accelerationApi.accelerationRequested(req.params.txid); res.status(200).send(); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/utils/api.ts b/backend/src/utils/api.ts new file mode 100644 index 000000000..69d746b9f --- /dev/null +++ b/backend/src/utils/api.ts @@ -0,0 +1,9 @@ +import { Request, Response } from 'express'; + +export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void { + if (req.accepts('json')) { + res.status(statusCode).json({ error: errorMessage }); + } else { + res.status(statusCode).send(errorMessage); + } +} \ No newline at end of file From f0af1703da0e73176d1a90ea956f53a9509f3a01 Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 24 Aug 2024 18:35:41 +0900 Subject: [PATCH 27/73] Release v3.0.0 --- backend/package-lock.json | 4 ++-- backend/package.json | 2 +- frontend/cypress/fixtures/mainnet_mempoolInfo.json | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- unfurler/package-lock.json | 4 ++-- unfurler/package.json | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 66e8d19a2..830c5de75 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.0.0", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { diff --git a/backend/package.json b/backend/package.json index 959516ac8..57a777d80 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0-rc1", + "version": "3.0.0", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/frontend/cypress/fixtures/mainnet_mempoolInfo.json b/frontend/cypress/fixtures/mainnet_mempoolInfo.json index d9e441277..70935be0b 100644 --- a/frontend/cypress/fixtures/mainnet_mempoolInfo.json +++ b/frontend/cypress/fixtures/mainnet_mempoolInfo.json @@ -750,7 +750,7 @@ }, "backendInfo": { "hostname": "node205.tk7.mempool.space", - "version": "3.0.0-rc1", + "version": "3.0.0", "gitCommit": "abbc8a134", "lightning": false }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a75c49bf3..fba2c48f7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.0.0", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", diff --git a/frontend/package.json b/frontend/package.json index b9fa4d3bc..536329fb6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0-rc1", + "version": "3.0.0", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json index 8c6e77883..287387180 100644 --- a/unfurler/package-lock.json +++ b/unfurler/package-lock.json @@ -1,12 +1,12 @@ { "name": "mempool-unfurl", - "version": "3.0.0-rc1", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mempool-unfurl", - "version": "3.0.0-rc1", + "version": "3.0.0", "dependencies": { "@types/node": "^16.11.41", "ejs": "^3.1.10", diff --git a/unfurler/package.json b/unfurler/package.json index c0d372e6f..76fcf5be3 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -1,6 +1,6 @@ { "name": "mempool-unfurl", - "version": "3.0.0-rc1", + "version": "3.0.0", "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", From c874d642c5e9b2bd91cdbfe65f9bf14b003a62c2 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 26 Aug 2024 21:38:16 +0900 Subject: [PATCH 28/73] Bump version to 3.1.0-dev --- backend/package-lock.json | 2 +- backend/package.json | 2 +- frontend/cypress/fixtures/mainnet_mempoolInfo.json | 2 +- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- unfurler/package-lock.json | 2 +- unfurler/package.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 830c5de75..2633de8c0 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/backend/package.json b/backend/package.json index 57a777d80..dab5546ec 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-backend", - "version": "3.0.0", + "version": "3.1.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/frontend/cypress/fixtures/mainnet_mempoolInfo.json b/frontend/cypress/fixtures/mainnet_mempoolInfo.json index 70935be0b..584364e9a 100644 --- a/frontend/cypress/fixtures/mainnet_mempoolInfo.json +++ b/frontend/cypress/fixtures/mainnet_mempoolInfo.json @@ -750,7 +750,7 @@ }, "backendInfo": { "hostname": "node205.tk7.mempool.space", - "version": "3.0.0", + "version": "3.1.0-dev", "gitCommit": "abbc8a134", "lightning": false }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fba2c48f7..14c50608b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/frontend/package.json b/frontend/package.json index 536329fb6..a940c9c15 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "mempool-frontend", - "version": "3.0.0", + "version": "3.1.0-dev", "description": "Bitcoin mempool visualizer and blockchain explorer backend", "license": "GNU Affero General Public License v3.0", "homepage": "https://mempool.space", diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json index 287387180..799148486 100644 --- a/unfurler/package-lock.json +++ b/unfurler/package-lock.json @@ -1,6 +1,6 @@ { "name": "mempool-unfurl", - "version": "3.0.0", + "version": "3.1.0-dev", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/unfurler/package.json b/unfurler/package.json index 76fcf5be3..bf3dad55b 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -1,6 +1,6 @@ { "name": "mempool-unfurl", - "version": "3.0.0", + "version": "3.1.0-dev", "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", From 4cc19a7235d16f0f53e4ea22bc557112f627dccb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:42:42 +0000 Subject: [PATCH 29/73] Bump axios from 1.7.2 to 1.7.4 in /backend Bumps [axios](https://github.com/axios/axios) from 1.7.2 to 1.7.4. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 17 +++++++++-------- backend/package.json | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 2633de8c0..126660166 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6,13 +6,14 @@ "packages": { "": { "name": "mempool-backend", - "version": "3.0.0", + "version": "3.1.0-dev", "hasInstallScript": true, "license": "GNU Affero General Public License v3.0", "dependencies": { + "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "~1.7.4", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.19.2", @@ -2277,9 +2278,9 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -9438,9 +9439,9 @@ "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" }, "axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/backend/package.json b/backend/package.json index dab5546ec..51abf2f7b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -42,7 +42,7 @@ "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.2", + "axios": "~1.7.4", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.19.2", From 4059a902a1a43449c164e1d0dbcc57958c490377 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:42:46 +0000 Subject: [PATCH 30/73] Bump tslib from 2.6.2 to 2.7.0 in /frontend Bumps [tslib](https://github.com/Microsoft/tslib) from 2.6.2 to 2.7.0. - [Release notes](https://github.com/Microsoft/tslib/releases) - [Commits](https://github.com/Microsoft/tslib/compare/v2.6.2...v2.7.0) --- updated-dependencies: - dependency-name: tslib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 26 ++++++++++++++++++-------- frontend/package.json | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 14c50608b..a802b85b6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "mempool-frontend", - "version": "3.0.0", + "version": "3.1.0-dev", "license": "GNU Affero General Public License v3.0", "dependencies": { "@angular-devkit/build-angular": "^17.3.1", @@ -42,7 +42,7 @@ "rxjs": "~7.8.1", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.7.0", "zone.js": "~0.14.4" }, "devDependencies": { @@ -699,6 +699,11 @@ "node": ">=10" } }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@angular-devkit/build-webpack": { "version": "0.1703.1", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.1.tgz", @@ -16925,9 +16930,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "node_modules/tuf-js": { "version": "2.2.0", @@ -18849,6 +18854,11 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" } } }, @@ -30763,9 +30773,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" }, "tuf-js": { "version": "2.2.0", diff --git a/frontend/package.json b/frontend/package.json index a940c9c15..12255e460 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -95,7 +95,7 @@ "esbuild": "^0.23.0", "tinyify": "^4.0.0", "tlite": "^0.1.9", - "tslib": "~2.6.0", + "tslib": "~2.7.0", "zone.js": "~0.14.4" }, "devDependencies": { From 0302999806e2d37f7516a860aa2ba688e9fb8a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:54:51 +0000 Subject: [PATCH 31/73] Bump elliptic from 6.5.4 to 6.5.7 in /frontend Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.5.7. - [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.5.7) --- updated-dependencies: - dependency-name: elliptic dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a802b85b6..2e9bb0353 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8810,9 +8810,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -24733,9 +24733,9 @@ "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==" }, "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", From 98cee4a6cdeae0f89238c15b563055c43ae72549 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 26 Aug 2024 15:34:04 +0000 Subject: [PATCH 32/73] [docs] update READMEs to newer node version --- backend/README.md | 2 +- frontend/README.md | 4 ++-- production/README.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/README.md b/backend/README.md index 6ae4ae3e2..cecc07bc9 100644 --- a/backend/README.md +++ b/backend/README.md @@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec) #### Build -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer_ _The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._ diff --git a/frontend/README.md b/frontend/README.md index 069f1d5f0..fb2a5e291 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -33,7 +33,7 @@ $ npm run config:defaults:liquid ### 3. Run the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Install project dependencies and run the frontend server: @@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already. ### 1. Build the Frontend -_Make sure to use Node.js 16.10 and npm 7._ +_Make sure to use Node.js 20.x and npm 9.x or newer._ Build the frontend: diff --git a/production/README.md b/production/README.md index 3f1b24d22..2805cde81 100644 --- a/production/README.md +++ b/production/README.md @@ -84,11 +84,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh- ### Node.js + npm -Build Node.js v16.16.0 and npm v8 from source using `nvm`: +Build Node.js v20.17.0 and npm v9 from source using `nvm`: ``` -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | zsh source $HOME/.zshrc -nvm install v16.16.0 --shared-zlib +nvm install v20.17.0 --shared-zlib nvm alias default node ``` From 185eae00e9af812032b37852d4551c2b4ef6a04a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 25 Aug 2024 22:38:00 +0000 Subject: [PATCH 33/73] Fix RBF tracking inconsistencies --- backend/src/api/rbf-cache.ts | 121 ++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a087abbe0..f4b192d3a 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -44,6 +44,22 @@ interface CacheEvent { value?: any, } +/** + * Singleton for tracking RBF trees + * + * Maintains a set of RBF trees, where each tree represents a sequence of + * consecutive RBF replacements. + * + * Trees are identified by the txid of the root transaction. + * + * To maintain consistency, the following invariants must be upheld: + * - Symmetry: replacedBy(A) = B <=> A in replaces(B) + * - Unique id: treeMap(treeMap(X)) = treeMap(X) + * - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B) + * - Existence: X in treeMap => treeMap(X) in rbfTrees + * - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap + */ + class RbfCache { private replacedBy: Map = new Map(); private replaces: Map = new Map(); @@ -61,6 +77,10 @@ class RbfCache { setInterval(this.cleanup.bind(this), 1000 * 60 * 10); } + /** + * Low level cache operations + */ + private addTx(txid: string, tx: MempoolTransactionExtended): void { this.txs.set(txid, tx); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); @@ -92,6 +112,12 @@ class RbfCache { this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); } + /** + * Basic data structure operations + * must uphold tree invariants + */ + + public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { return; @@ -114,6 +140,10 @@ class RbfCache { if (!replacedTx.rbf) { txFullRbf = true; } + if (this.replacedBy.has(replacedTx.txid)) { + // should never happen + continue; + } this.replacedBy.set(replacedTx.txid, newTx.txid); if (this.treeMap.has(replacedTx.txid)) { const treeId = this.treeMap.get(replacedTx.txid); @@ -140,18 +170,47 @@ class RbfCache { } } newTx.fullRbf = txFullRbf; - const treeId = replacedTrees[0].tx.txid; const newTree = { tx: newTx, time: newTime, fullRbf: treeFullRbf, replaces: replacedTrees }; - this.addTree(treeId, newTree); - this.updateTreeMap(treeId, newTree); + this.addTree(newTree.tx.txid, newTree); + this.updateTreeMap(newTree.tx.txid, newTree); this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); } + public mined(txid): void { + if (!this.txs.has(txid)) { + return; + } + const treeId = this.treeMap.get(txid); + if (treeId && this.rbfTrees.has(treeId)) { + const tree = this.rbfTrees.get(treeId); + if (tree) { + this.setTreeMined(tree, txid); + tree.mined = true; + this.dirtyTrees.add(treeId); + this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); + } + } + this.evict(txid); + } + + // flag a transaction as removed from the mempool + public evict(txid: string, fast: boolean = false): void { + this.evictionCount++; + if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { + const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours + this.addExpiration(txid, expiryTime); + } + } + + /** + * Read-only public interface + */ + public has(txId: string): boolean { return this.txs.has(txId); } @@ -232,32 +291,6 @@ class RbfCache { return changes; } - public mined(txid): void { - if (!this.txs.has(txid)) { - return; - } - const treeId = this.treeMap.get(txid); - if (treeId && this.rbfTrees.has(treeId)) { - const tree = this.rbfTrees.get(treeId); - if (tree) { - this.setTreeMined(tree, txid); - tree.mined = true; - this.dirtyTrees.add(treeId); - this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); - } - } - this.evict(txid); - } - - // flag a transaction as removed from the mempool - public evict(txid: string, fast: boolean = false): void { - this.evictionCount++; - if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { - const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours - this.addExpiration(txid, expiryTime); - } - } - // is the transaction involved in a full rbf replacement? public isFullRbf(txid: string): boolean { const treeId = this.treeMap.get(txid); @@ -271,6 +304,10 @@ class RbfCache { return tree?.fullRbf; } + /** + * Cache maintenance & utility functions + */ + private cleanup(): void { const now = Date.now(); for (const txid of this.expiring.keys()) { @@ -299,10 +336,6 @@ class RbfCache { for (const tx of (replaces || [])) { // recursively remove prior versions from the cache this.replacedBy.delete(tx); - // if this is the id of a tree, remove that too - if (this.treeMap.get(tx) === tx) { - this.removeTree(tx); - } this.remove(tx); } } @@ -376,8 +409,15 @@ class RbfCache { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; - for (const deflatedTree of trees) { - await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { + const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + if (tree) { + this.addTree(tree.tx.txid, tree); + this.updateTreeMap(tree.tx.txid, tree); + if (tree.mined) { + this.evict(tree.tx.txid); + } + } } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -426,6 +466,12 @@ class RbfCache { return; } + // if this tx is already in the cache, return early + if (this.treeMap.has(txid)) { + this.removeTree(deflated.key); + return; + } + // recursively reconstruct child trees for (const childId of treeInfo.replaces) { const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); @@ -457,10 +503,6 @@ class RbfCache { fullRbf: treeInfo.fullRbf, replaces, }; - this.treeMap.set(txid, root); - if (root === txid) { - this.addTree(root, tree); - } return tree; } @@ -511,6 +553,7 @@ class RbfCache { processTxs(txs); } + // evict missing transactions for (const txid of txids) { if (!found[txid]) { this.evict(txid, false); From e362003746e237adbfc2681695b956f05ece80ef Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 26 Aug 2024 21:51:49 +0000 Subject: [PATCH 34/73] Catch RBF replacements across mempool update boundaries --- backend/src/api/common.ts | 16 +++++++------- backend/src/api/mempool.ts | 31 ++++++++++++---------------- backend/src/api/websocket-handler.ts | 19 +++++++++++++---- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 13fc86147..f3d3e43b5 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -80,8 +80,8 @@ export class Common { return arr; } - static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { - const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; + static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; // For small N, a naive nested loop is extremely fast, but it doesn't scale if (added.length < 1000 && deleted.length < 50 && !forceScalable) { @@ -96,7 +96,7 @@ export class Common { addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); }); if (foundMatches?.length) { - matches[addedTx.txid] = [...new Set(foundMatches)]; + matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; } }); } else { @@ -124,7 +124,7 @@ export class Common { foundMatches.add(deletedTx); } if (foundMatches.size) { - matches[addedTx.txid] = [...foundMatches]; + matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; } } } @@ -139,17 +139,17 @@ export class Common { const replaced: Set = new Set(); for (let i = 0; i < tx.vin.length; i++) { const vin = tx.vin[i]; - const match = spendMap.get(`${vin.txid}:${vin.vout}`); + const key = `${vin.txid}:${vin.vout}`; + const match = spendMap.get(key); if (match && match.txid !== tx.txid) { replaced.add(match); // remove this tx from the spendMap // prevents the same tx being replaced more than once for (const replacedVin of match.vin) { - const key = `${replacedVin.txid}:${replacedVin.vout}`; - spendMap.delete(key); + const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; + spendMap.delete(replacedKey); } } - const key = `${vin.txid}:${vin.vout}`; spendMap.delete(key); } if (replaced.size) { diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 1f55179fb..1442b05fa 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -19,12 +19,13 @@ class Mempool { private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {}; private spendMap = new Map(); + private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined; private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; private accelerations: { [txId: string]: Acceleration } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; @@ -74,12 +75,12 @@ class Mempool { } public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void { this.mempoolChangedCallback = fn; } public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise): void { this.$asyncMempoolChangedCallback = fn; } @@ -362,12 +363,15 @@ class Mempool { const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; - if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { - this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); + this.recentlyDeleted.unshift(deletedTransactions); + this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates + + if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) { + this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta); } - if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) { + if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { this.updateTimerProgress(timer, 'running async mempool callback'); - await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates); + await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates); this.updateTimerProgress(timer, 'completed async mempool callback'); } @@ -541,16 +545,7 @@ class Mempool { } } - public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { - for (const rbfTransaction in rbfTransactions) { - if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) { - // Store replaced transactions - rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]); - } - } - } - - public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { + public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { for (const rbfTransaction in rbfTransactions) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { // Store replaced transactions diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 79a783f88..2a047472e 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -520,8 +520,17 @@ class WebsocketHandler { } } + /** + * + * @param newMempool + * @param mempoolSize + * @param newTransactions array of transactions added this mempool update. + * @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first. + * @param accelerationDelta + * @param candidates + */ async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates): Promise { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -529,6 +538,8 @@ class WebsocketHandler { this.printLogs(); + const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : []; + const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); let added = newTransactions; let removed = deletedTransactions; @@ -547,7 +558,7 @@ class WebsocketHandler { const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mempoolInfo = memPool.getMempoolInfo(); const vBytesPerSecond = memPool.getVBytesPerSecond(); - const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); + const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const da = difficultyAdjustment.getDifficultyAdjustment(); const accelerations = memPool.getAccelerations(); memPool.handleRbfTransactions(rbfTransactions); @@ -578,7 +589,7 @@ class WebsocketHandler { const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const tx of newTransactions) { if (rbfTransactions[tx.txid]) { - for (const replaced of rbfTransactions[tx.txid]) { + for (const replaced of rbfTransactions[tx.txid].replaced) { replacedTransactions.push({ replaced: replaced.txid, by: tx }); } } @@ -947,7 +958,7 @@ class WebsocketHandler { await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); - memPool.handleMinedRbfTransactions(rbfTransactions); + memPool.handleRbfTransactions(rbfTransactions); memPool.removeFromSpendMap(transactions); if (config.MEMPOOL.AUDIT && memPool.isInSync()) { From ee53597fe9805ce8c5de3b6e17deac7b3159cb30 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 26 Aug 2024 23:22:39 +0000 Subject: [PATCH 35/73] Resume RBF trees after restart --- backend/src/api/disk-cache.ts | 1 + backend/src/api/rbf-cache.ts | 27 ++++++++++++++++++++++++++- backend/src/api/redis-cache.ts | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 202f8f4cb..f2a1f2390 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -257,6 +257,7 @@ class DiskCache { trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } } catch (e) { diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index f4b192d3a..944ad790e 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -403,7 +403,7 @@ class RbfCache { }; } - public async load({ txs, trees, expiring, mempool }): Promise { + public async load({ txs, trees, expiring, mempool, spendMap }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); @@ -425,6 +425,31 @@ class RbfCache { } }); this.staleCount = 0; + + // connect cached trees to current mempool transactions + const conflicts: Record }> = {}; + for (const tree of this.rbfTrees.values()) { + const tx = this.getTx(tree.tx.txid); + if (!tx || tree.mined) { + continue; + } + for (const vin of tx.vin) { + const conflict = spendMap.get(`${vin.txid}:${vin.vout}`); + if (conflict && conflict.txid !== tx.txid) { + if (!conflicts[conflict.txid]) { + conflicts[conflict.txid] = { + replacedBy: conflict, + replaces: new Set(), + }; + } + conflicts[conflict.txid].replaces.add(tx); + } + } + } + for (const { replacedBy, replaces } of Object.values(conflicts)) { + this.add([...replaces.values()], replacedBy); + } + await this.checkTrees(); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.cleanup(); diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index cbfa2f18b..1caade15b 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -365,6 +365,7 @@ class RedisCache { trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } From 9e05060af4de8411536fccc41098c69ae3775155 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 27 Aug 2024 00:17:17 +0000 Subject: [PATCH 36/73] fix tests --- backend/src/__tests__/api/common.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 74a7db88f..14ae3c78b 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -1,5 +1,5 @@ import { Common } from '../../api/common'; -import { MempoolTransactionExtended } from '../../mempool.interfaces'; +import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; const randomTransactions = require('./test-data/transactions-random.json'); const replacedTransactions = require('./test-data/transactions-replaced.json'); @@ -10,14 +10,14 @@ describe('Common', () => { describe('RBF', () => { const newTransactions = rbfTransactions.concat(randomTransactions); test('should detect RBF transactions with fast method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); }); test('should detect RBF transactions with scalable method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); From a3e61525fe76af94494be02f6a4c24e565aac2eb Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 27 Aug 2024 11:42:13 +0200 Subject: [PATCH 37/73] Reset acceleration flow state when leaving transaction --- frontend/src/app/components/transaction/transaction.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..6ff85c5bd 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -966,6 +966,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.filters = []; this.showCpfpDetails = false; this.showAccelerationDetails = false; + this.accelerationFlowCompleted = false; this.accelerationInfo = null; this.cashappEligible = false; this.txInBlockIndex = null; From 624b3473fc39ba34a1f13f82579379ee39e553d7 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 27 Aug 2024 11:29:29 +0200 Subject: [PATCH 38/73] Hide accelerator panel if tx gets accelerated on another session --- .../accelerate-checkout/accelerate-checkout.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6b1eadf7d..0bb37f15e 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -196,9 +196,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (changes.scrollEvent && this.scrollEvent) { this.scrollToElement('acceleratePreviewAnchor', 'start'); } - if (changes.accelerating) { - if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) { + if (changes.accelerating && this.accelerating) { + if (this.step === 'processing' || this.step === 'paid') { this.moveToStep('success'); + } else { // Edge case where the transaction gets accelerated by someone else or on another session + this.closeModal(); } } } From 1ea45e9e96acab6901297f2f82793605a624d5fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 02:59:35 +0000 Subject: [PATCH 39/73] Bump cypress from 13.13.0 to 13.14.0 in /frontend Bumps [cypress](https://github.com/cypress-io/cypress) from 13.13.0 to 13.14.0. - [Release notes](https://github.com/cypress-io/cypress/releases) - [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md) - [Commits](https://github.com/cypress-io/cypress/compare/v13.13.0...v13.14.0) --- updated-dependencies: - dependency-name: cypress dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 19 ++++++++++--------- frontend/package.json | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2e9bb0353..c17e706af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", + "cypress": "^13.14.0", "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.23.0", @@ -62,7 +63,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.13.0", + "cypress": "^13.14.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -8045,13 +8046,13 @@ "peer": true }, "node_modules/cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "version": "13.14.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", + "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@cypress/request": "^3.0.0", + "@cypress/request": "^3.0.1", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -24137,12 +24138,12 @@ "peer": true }, "cypress": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz", - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==", + "version": "13.14.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", + "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", "optional": true, "requires": { - "@cypress/request": "^3.0.0", + "@cypress/request": "^3.0.1", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", diff --git a/frontend/package.json b/frontend/package.json index 12255e460..3b5d61be0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -115,7 +115,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.13.0", + "cypress": "^13.14.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", From 98d98b2478320d1e868a1f5df7cf4243b7a675b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 05:01:52 +0000 Subject: [PATCH 40/73] Bump micromatch from 4.0.4 to 4.0.8 in /frontend Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.4 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.4...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c17e706af..16400db7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,6 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", - "cypress": "^13.14.0", "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.23.0", @@ -12694,12 +12693,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -27622,12 +27621,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "miller-rabin": { From b526ee0877f3a9c9ff4fef9ed6414221e424185c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 28 Aug 2024 14:38:12 +0000 Subject: [PATCH 41/73] Handle paginated acceleration results --- .../block/block-preview.component.ts | 2 +- .../app/components/block/block.component.ts | 2 +- .../components/tracker/tracker.component.ts | 2 +- .../transaction/transaction.component.ts | 2 +- .../src/app/services/services-api.service.ts | 25 ++++++++++++++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index 72da96818..572f91a38 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { }) ), this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 5cba85e90..9da74cb62 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -319,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.accelerationsSubscription = this.block$.pipe( switchMap((block) => { return this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 24b5fc1dc..42156d2a9 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -286,7 +286,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.accelerationInfo = null; }), switchMap((blockHash: string) => { - return this.servicesApiService.getAccelerationHistory$({ blockHash }); + return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId); }), catchError(() => { return of(null); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..09e0d2874 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -343,7 +343,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe( + return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).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'); diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 1366342f7..5213e131c 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { StateService } from './state.service'; import { StorageService } from './storage.service'; import { MenuGroup } from '../interfaces/services.interface'; -import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; +import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap, map } from 'rxjs'; import { IBackendInfo } from '../interfaces/websocket.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; @@ -160,6 +160,29 @@ export class ServicesApiServices { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } }); } + getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable { + const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => { + return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe( + map((response) => ({ + page, + total: parseInt(response.headers.get('X-Total-Count'), 10), + accelerations: accelerations.concat(response.body || []), + })), + switchMap(({page, total, accelerations}) => { + if (accelerations.length >= Math.min(total, limit ?? Infinity) || (findTxid && accelerations.find((acc) => acc.txid === findTxid))) { + return of({ page, total, accelerations }); + } else { + return getPage$(page + 1, accelerations); + } + }), + ); + }; + + return getPage$(1).pipe( + map(({ accelerations }) => accelerations), + ); + } + getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); } From 0a5a2c3c7e40c46c2d2ddead1144c48d7c718038 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 28 Aug 2024 16:50:00 +0200 Subject: [PATCH 42/73] Remove difficulty epoch block offset --- .../difficulty-mining/difficulty-mining.component.ts | 2 +- .../src/app/components/difficulty/difficulty.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index 90b41d749..e19f510b5 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - remainingBlocks: da.remainingBlocks - 1, + remainingBlocks: da.remainingBlocks, colorAdjustments, colorPreviousAdjustments, newDifficultyHeight: da.nextRetargetHeight, diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index 579b49fc3..6a99aecef 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - minedBlocks: this.currentIndex + 1, - remainingBlocks: da.remainingBlocks - 1, + minedBlocks: this.currentIndex, + remainingBlocks: da.remainingBlocks, expectedBlocks: Math.floor(da.expectedBlocks), colorAdjustments, colorPreviousAdjustments, From fad39e0bea1fce20e22ad200cdc2b6fee0bd69fe Mon Sep 17 00:00:00 2001 From: orangesurf Date: Thu, 29 Aug 2024 13:02:32 +0200 Subject: [PATCH 43/73] Update about page enterprise sponsors --- .../app/components/about/about.component.html | 26 ++++++++++++------- .../app/components/about/about.component.scss | 9 +++++++ frontend/src/resources/profile/bitkey.svg | 3 +++ frontend/src/resources/profile/leather.svg | 4 +++ 4 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 frontend/src/resources/profile/bitkey.svg create mode 100644 frontend/src/resources/profile/leather.svg diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 1af8d8e62..406835572 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -53,7 +53,7 @@ Spiral - + Unchained - - - - - - - - Gemini + + + Bitkey Exodus + + + + + + + + Gemini + + + + Leather +

diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 41e9209b7..6a20239cc 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -251,3 +251,12 @@ width: 64px; height: 64px; } + +.enterprise-sponsor { + .wrapper { + display: flex; + flex-wrap: wrap; + justify-content: center; + max-width: 800px; + } +} \ No newline at end of file diff --git a/frontend/src/resources/profile/bitkey.svg b/frontend/src/resources/profile/bitkey.svg new file mode 100644 index 000000000..875436402 --- /dev/null +++ b/frontend/src/resources/profile/bitkey.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/resources/profile/leather.svg b/frontend/src/resources/profile/leather.svg new file mode 100644 index 000000000..20fe2c28b --- /dev/null +++ b/frontend/src/resources/profile/leather.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From eab008c7075f4a806505c4dd0c0a8038cf58bfc0 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 26 Aug 2024 17:47:56 +0200 Subject: [PATCH 44/73] Ineligible transaction link to accelerator FAQ --- .../transaction/transaction.component.html | 28 +++++++++--------- .../transaction/transaction.component.scss | 29 ++++++------------- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 715fca4c8..31fa9a6ac 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -551,23 +551,23 @@ ETA - @if (eta.blocks >= 7) { - - Not any time soon - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { - Accelerate - } - - } @else if (network === 'liquid' || network === 'liquidtestnet') { + @if (network === 'liquid' || network === 'liquidtestnet') { } @else { - - - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { - Accelerate + + @if (eta.blocks >= 7) { + Not any time soon + } @else { + + } + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary) { + } - - } diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 232a2cacb..1706dfcab 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -287,37 +287,21 @@ } .accelerate { - display: flex !important; - align-self: auto; - margin-left: auto; - background-color: var(--tertiary); - @media (max-width: 849px) { - margin-left: 5px; - } + @media (min-width: 850px) { + margin-left: auto; + } } .etaDeepMempool { - justify-content: flex-end; flex-wrap: wrap; - align-content: center; - @media (max-width: 995px) { - justify-content: left !important; - } @media (max-width: 849px) { justify-content: right !important; } } .accelerateDeepMempool { - align-self: auto; - margin-left: auto; background-color: var(--tertiary); - @media (max-width: 995px) { - margin-left: 0px; - } - @media (max-width: 849px) { - margin-left: 5px; - } + margin-left: 5px; } .goggles-icon { @@ -335,4 +319,9 @@ .oobFees { color: #905cf4; +} + +.disabled { + opacity: 0.5; + pointer-events: none; } \ No newline at end of file From 12285465d9ea3a034f6b054a158cf63e9ebbd2ce Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 30 Aug 2024 21:39:22 +0000 Subject: [PATCH 45/73] Add support for anchor output type --- backend/src/api/bitcoin/bitcoin-api.ts | 1 + .../address-labels/address-labels.component.ts | 2 +- frontend/src/app/shared/address-utils.ts | 11 +++++++++++ .../address-type/address-type.component.html | 3 +++ frontend/src/app/shared/script.utils.ts | 1 + 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 3e1fe2108..7fa431db6 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -323,6 +323,7 @@ class BitcoinApi implements AbstractBitcoinApi { 'witness_v1_taproot': 'v1_p2tr', 'nonstandard': 'nonstandard', 'multisig': 'multisig', + 'anchor': 'anchor', 'nulldata': 'op_return' }; diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index dd81b9809..ff3c27240 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges { } handleVin() { - const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]) + const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]); if (address?.scripts.size) { const script = address?.scripts.values().next().value; if (script.template?.label) { diff --git a/frontend/src/app/shared/address-utils.ts b/frontend/src/app/shared/address-utils.ts index 92646af14..59c85014b 100644 --- a/frontend/src/app/shared/address-utils.ts +++ b/frontend/src/app/shared/address-utils.ts @@ -17,6 +17,7 @@ export type AddressType = 'fee' | 'v0_p2wsh' | 'v1_p2tr' | 'confidential' + | 'anchor' | 'unknown' const ADDRESS_PREFIXES = { @@ -188,6 +189,12 @@ export class AddressTypeInfo { const v = vin[0]; this.processScript(new ScriptInfo('scriptpubkey', v.prevout.scriptpubkey, v.prevout.scriptpubkey_asm)); } + } else if (this.type === 'unknown') { + for (const v of vin) { + if (v.prevout?.scriptpubkey === '51024e73') { + this.type = 'anchor'; + } + } } // and there's nothing more to learn from processing inputs for other types } @@ -197,6 +204,10 @@ export class AddressTypeInfo { if (!this.scripts.size) { this.processScript(new ScriptInfo('scriptpubkey', output.scriptpubkey, output.scriptpubkey_asm)); } + } else if (this.type === 'unknown') { + if (output.scriptpubkey === '51024e73') { + this.type = 'anchor'; + } } } diff --git a/frontend/src/app/shared/components/address-type/address-type.component.html b/frontend/src/app/shared/components/address-type/address-type.component.html index fe4286689..598c21a6e 100644 --- a/frontend/src/app/shared/components/address-type/address-type.component.html +++ b/frontend/src/app/shared/components/address-type/address-type.component.html @@ -20,6 +20,9 @@ @case ('multisig') { bare multisig } + @case ('anchor') { + anchor + } @case (null) { unknown } diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 171112dcc..637eede30 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -166,6 +166,7 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate ln_anchor: () => ({ type: 'ln_anchor', label: 'Lightning Anchor' }), ln_anchor_swept: () => ({ type: 'ln_anchor_swept', label: 'Swept Lightning Anchor' }), multisig: (m: number, n: number) => ({ type: 'multisig', m, n, label: $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:` }), + anchor: () => ({ type: 'anchor', label: 'anchor' }), }; export class ScriptInfo { From 099d84a39551a49b417e23a1ac4c02823ec6f6dd Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 30 Aug 2024 23:12:24 +0000 Subject: [PATCH 46/73] New standardness rules for v3 & anchor outputs, with activation height logic --- backend/src/api/blocks.ts | 16 ++--- backend/src/api/common.ts | 67 ++++++++++++++++--- backend/src/repositories/BlocksRepository.ts | 2 +- .../components/tracker/tracker.component.ts | 2 +- .../transaction/transaction.component.ts | 2 +- frontend/src/app/shared/transaction.utils.ts | 60 +++++++++++++++-- 6 files changed, 123 insertions(+), 26 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index a5b8af0e2..306179ca5 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -219,10 +219,10 @@ class Blocks { }; } - public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary { + public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary { return { id: hash, - transactions: Common.classifyTransactions(transactions), + transactions: Common.classifyTransactions(transactions, height), }; } @@ -616,7 +616,7 @@ class Blocks { // add CPFP const cpfpSummary = calculateGoodBlockCpfp(height, txs, []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2); if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) { const cpfpClusters = await CpfpRepository.$getClustersAt(height); @@ -653,7 +653,7 @@ class Blocks { } const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []); // classify - const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions); + const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions); const classifiedTxMap: { [txid: string]: TransactionClassified } = {}; for (const tx of classifiedTxs) { classifiedTxMap[tx.txid] = tx; @@ -912,7 +912,7 @@ class Blocks { } const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta }))); const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions); - const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions); + const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions); this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`); if (Common.indexingEnabled()) { @@ -1169,7 +1169,7 @@ class Blocks { transactions: cpfpSummary.transactions.map(tx => { let flags: number = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e)); } @@ -1188,7 +1188,7 @@ class Blocks { } else { if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(hash, txs); + summary = this.summarizeBlockTransactions(hash, height || 0, txs); summaryVersion = 1; } else { // Call Core RPC @@ -1324,7 +1324,7 @@ class Blocks { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx)); - summary = this.summarizeBlockTransactions(cleanBlock.hash, txs); + summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs); summaryVersion = 1; } else { // Call Core RPC diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 13fc86147..d17068a09 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -10,7 +10,6 @@ import logger from '../logger'; import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script'; // Bitcoin Core default policy settings -const TX_MAX_STANDARD_VERSION = 2; const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); @@ -200,10 +199,13 @@ export class Common { * * returns true early if any standardness rule is violated, otherwise false * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) + * + * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks. + * For now, just pull out individual rules into versioned functions where necessary. */ - static isNonStandard(tx: TransactionExtended): boolean { + static isNonStandard(tx: TransactionExtended, height?: number): boolean { // version - if (tx.version > TX_MAX_STANDARD_VERSION) { + if (this.isNonStandardVersion(tx, height)) { return true; } @@ -250,6 +252,8 @@ export class Common { } } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { return true; + } else if (this.isNonStandardAnchor(tx, height)) { + return true; } // TODO: bad-witness-nonstandard } @@ -335,6 +339,49 @@ export class Common { return false; } + // Individual versioned standardness rules + + static V3_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean { + let TX_MAX_STANDARD_VERSION = 3; + if ( + height != null + && this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + ) { + // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + TX_MAX_STANDARD_VERSION = 2; + } + + if (tx.version > TX_MAX_STANDARD_VERSION) { + return true; + } + return false; + } + + static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, + }; + static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean { + if ( + height != null + && this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + && height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK] + ) { + // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + return true; + } + return false; + } + static getNonWitnessSize(tx: TransactionExtended): number { let weight = tx.weight; let hasWitness = false; @@ -415,7 +462,7 @@ export class Common { return flags; } - static getTransactionFlags(tx: TransactionExtended): number { + static getTransactionFlags(tx: TransactionExtended, height?: number): number { let flags = tx.flags ? BigInt(tx.flags) : 0n; // Update variable flags (CPFP, RBF) @@ -548,7 +595,7 @@ export class Common { if (hasFakePubkey) { flags |= TransactionFlags.fake_pubkey; } - + // fast but bad heuristic to detect possible coinjoins // (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse) const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1; @@ -564,17 +611,17 @@ export class Common { flags |= TransactionFlags.batch_payout; } - if (this.isNonStandard(tx)) { + if (this.isNonStandard(tx, height)) { flags |= TransactionFlags.nonstandard; } return Number(flags); } - static classifyTransaction(tx: TransactionExtended): TransactionClassified { + static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified { let flags = 0; try { - flags = Common.getTransactionFlags(tx); + flags = Common.getTransactionFlags(tx, height); } catch (e) { logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e)); } @@ -585,8 +632,8 @@ export class Common { }; } - static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] { - return txs.map(Common.classifyTransaction); + static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] { + return txs.map(tx => Common.classifyTransaction(tx, height)); } static stripTransaction(tx: TransactionExtended): TransactionStripped { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index 90100a767..de6c1deb8 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -1106,7 +1106,7 @@ class BlocksRepository { let summaryVersion = 0; if (config.MEMPOOL.BACKEND === 'esplora') { const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx)); - summary = blocks.summarizeBlockTransactions(dbBlk.id, txs); + summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs); summaryVersion = 1; } else { // Call Core RPC diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 24b5fc1dc..3b0f53e9c 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -747,7 +747,7 @@ export class TrackerComponent implements OnInit, OnDestroy { checkAccelerationEligibility() { if (this.tx) { - this.tx.flags = getTransactionFlags(this.tx); + this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network); const replaceableInputs = (this.tx.flags & (TransactionFlags.sighash_none | TransactionFlags.sighash_acp)) > 0n; const highSigop = (this.tx.sigops * 20) > this.tx.weight; this.eligibleForAcceleration = !replaceableInputs && !highSigop; diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..c80006552 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -901,7 +901,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit'); this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot'); this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf'); - this.tx.flags = getTransactionFlags(this.tx); + this.tx.flags = getTransactionFlags(this.tx, null, null, this.tx.status?.block_time, this.stateService.network); this.filters = this.tx.flags ? toFilters(this.tx.flags).filter(f => f.txPage) : []; this.checkAccelerationEligibility(); } else { diff --git a/frontend/src/app/shared/transaction.utils.ts b/frontend/src/app/shared/transaction.utils.ts index c13616c60..bbf28a250 100644 --- a/frontend/src/app/shared/transaction.utils.ts +++ b/frontend/src/app/shared/transaction.utils.ts @@ -2,9 +2,9 @@ import { TransactionFlags } from './filters.utils'; import { getVarIntLength, opcodes, parseMultisigScript, isPoint } from './script.utils'; import { Transaction } from '../interfaces/electrs.interface'; import { CpfpInfo, RbfInfo, TransactionStripped } from '../interfaces/node-api.interface'; +import { StateService } from '../services/state.service'; // Bitcoin Core default policy settings -const TX_MAX_STANDARD_VERSION = 2; const MAX_STANDARD_TX_WEIGHT = 400_000; const MAX_BLOCK_SIGOPS_COST = 80_000; const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5); @@ -89,10 +89,13 @@ export function isDERSig(w: string): boolean { * * returns true early if any standardness rule is violated, otherwise false * (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced) + * + * As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks. + * For now, just pull out individual rules into versioned functions where necessary. */ -export function isNonStandard(tx: Transaction): boolean { +export function isNonStandard(tx: Transaction, height?: number, network?: string): boolean { // version - if (tx.version > TX_MAX_STANDARD_VERSION) { + if (isNonStandardVersion(tx, height, network)) { return true; } @@ -139,6 +142,8 @@ export function isNonStandard(tx: Transaction): boolean { } } else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) { return true; + } else if (isNonStandardAnchor(tx, height, network)) { + return true; } // TODO: bad-witness-nonstandard } @@ -203,6 +208,51 @@ export function isNonStandard(tx: Transaction): boolean { return false; } +// Individual versioned standardness rules + +const V3_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, +}; +function isNonStandardVersion(tx: Transaction, height?: number, network?: string): boolean { + let TX_MAX_STANDARD_VERSION = 3; + if ( + height != null + && network != null + && V3_STANDARDNESS_ACTIVATION_HEIGHT[network] + && height <= V3_STANDARDNESS_ACTIVATION_HEIGHT[network] + ) { + // V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + TX_MAX_STANDARD_VERSION = 2; + } + + if (tx.version > TX_MAX_STANDARD_VERSION) { + return true; + } + return false; +} + +const ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = { + 'testnet4': 42_000, + 'testnet': 2_900_000, + 'signet': 211_000, + '': 863_500, +}; +function isNonStandardAnchor(tx: Transaction, height?: number, network?: string): boolean { + if ( + height != null + && network != null + && ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network] + && height <= ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[network] + ) { + // anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891) + return true; + } + return false; +} + // A witness program is any valid scriptpubkey that consists of a 1-byte push opcode // followed by a data push between 2 and 40 bytes. // https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240 @@ -289,7 +339,7 @@ export function isBurnKey(pubkey: string): boolean { ].includes(pubkey); } -export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean): bigint { +export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replacement?: boolean, height?: number, network?: string): bigint { let flags = tx.flags ? BigInt(tx.flags) : 0n; // Update variable flags (CPFP, RBF) @@ -439,7 +489,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac flags |= TransactionFlags.batch_payout; } - if (isNonStandard(tx)) { + if (isNonStandard(tx, height, network)) { flags |= TransactionFlags.nonstandard; } From e44f30d7a791cb8a54b717f476f227c58e8612d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Strnad?= <43024885+vostrnad@users.noreply.github.com> Date: Sat, 31 Aug 2024 14:31:55 +0200 Subject: [PATCH 47/73] Allow OP_0 in multisig scripts --- backend/src/utils/bitcoin-script.ts | 4 ++-- frontend/src/app/bitcoin.utils.ts | 4 ++-- frontend/src/app/shared/script.utils.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 3414e8269..8f551aa23 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opN) { return; } - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); @@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb if (!opM) { return; } - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 92d3de7f3..ae522121c 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb return; } const opN = ops.pop(); - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)[0], 10); @@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb } } const opM = ops.pop(); - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)[0], 10); diff --git a/frontend/src/app/shared/script.utils.ts b/frontend/src/app/shared/script.utils.ts index 171112dcc..fdb0373c9 100644 --- a/frontend/src/app/shared/script.utils.ts +++ b/frontend/src/app/shared/script.utils.ts @@ -266,7 +266,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n: if (!opN) { return; } - if (!opN.startsWith('OP_PUSHNUM_')) { + if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) { return; } const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10); @@ -286,7 +286,7 @@ export function parseMultisigScript(script: string): undefined | { m: number, n: if (!opM) { return; } - if (!opM.startsWith('OP_PUSHNUM_')) { + if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) { return; } const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10); From 8ab104d1911b2ed150ebe3bd6ec2ce7b30a52388 Mon Sep 17 00:00:00 2001 From: orangesurf Date: Mon, 2 Sep 2024 13:47:41 +0200 Subject: [PATCH 48/73] switch to alternate logo --- frontend/src/resources/profile/leather.svg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/resources/profile/leather.svg b/frontend/src/resources/profile/leather.svg index 20fe2c28b..a909606fa 100644 --- a/frontend/src/resources/profile/leather.svg +++ b/frontend/src/resources/profile/leather.svg @@ -1,4 +1,3 @@ - - - + + \ No newline at end of file From f6fac92180b6253fc587d05973861b22ed21b6a3 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Wed, 4 Sep 2024 13:52:48 +0200 Subject: [PATCH 49/73] [faucet] add missing error message for suspicious twitter accounts --- .../src/app/components/faucet/faucet.component.html | 11 +++++++++-- .../src/app/components/faucet/faucet.component.ts | 2 +- frontend/src/app/shared/shared.module.ts | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/faucet/faucet.component.html b/frontend/src/app/components/faucet/faucet.component.html index 89e6bb8a8..0f0307e54 100644 --- a/frontend/src/app/components/faucet/faucet.component.html +++ b/frontend/src/app/components/faucet/faucet.component.html @@ -5,7 +5,7 @@
- + @if (txid) {
@@ -36,6 +36,13 @@
} + @else if (error === 'account_limited') { +
+
+ Your Twitter account does not allow you to access the faucet +
+
+ } @else if (error) { @@ -81,7 +88,7 @@ } - @if (status?.address) { + @if (status?.address) {
If you no longer need your testnet4 coins, please consider sending them back to replenish the faucet.
} diff --git a/frontend/src/app/components/faucet/faucet.component.ts b/frontend/src/app/components/faucet/faucet.component.ts index 891b6310d..566a3b970 100644 --- a/frontend/src/app/components/faucet/faucet.component.ts +++ b/frontend/src/app/components/faucet/faucet.component.ts @@ -19,7 +19,7 @@ export class FaucetComponent implements OnInit, OnDestroy { error: string = ''; user: any = undefined; txid: string = ''; - + faucetStatusSubscription: Subscription; status: { min: number; // minimum amount to request at once (in sats) diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2d5b4d0f9..6221f397d 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, - faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark} from '@fortawesome/free-solid-svg-icons'; + faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline, faCircleXmark, faCalendarCheck } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MenuComponent } from '../components/menu/menu.component'; import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component'; @@ -440,5 +440,6 @@ export class SharedModule { library.addIcons(faFaucetDrip); library.addIcons(faTimeline); library.addIcons(faCircleXmark); + library.addIcons(faCalendarCheck); } } From 07fd3d3409d1ada0cf6f92c577c48710466078c9 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 4 Sep 2024 22:26:08 +0900 Subject: [PATCH 50/73] ops: Bump some FreeBSD install packages --- production/install | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/install b/production/install index 30754863c..bf7153557 100755 --- a/production/install +++ b/production/install @@ -392,9 +392,9 @@ DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev liba # packages needed for mempool ecosystem FREEBSD_PKG=() FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim) -FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4) +FREEBSD_PKG+=(openssh-portable py311-pip rust llvm18 jq base64 libzmq4) FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf) -FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase) +FREEBSD_PKG+=(nginx rsync py311-certbot-nginx mariadb1011-server) FREEBSD_PKG+=(geoipupdate redis) FREEBSD_UNFURL_PKG=() From 64223c4744429016b2ebde7ac092241e4b04c749 Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 5 Sep 2024 02:15:08 +0900 Subject: [PATCH 51/73] ops: Set blocksxor=0 in bitcoin.conf --- production/bitcoin.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/production/bitcoin.conf b/production/bitcoin.conf index 1b4eb1171..63baa32b5 100644 --- a/production/bitcoin.conf +++ b/production/bitcoin.conf @@ -15,6 +15,7 @@ rpcpassword=__BITCOIN_RPC_PASS__ whitelist=127.0.0.1 whitelist=103.99.168.0/22 whitelist=2401:b140::/32 +blocksxor=0 #uacomment=@wiz [main] From dbe774cc64e5b523953fc7ead08c25f8822d2532 Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 9 Sep 2024 02:45:16 +0900 Subject: [PATCH 52/73] ops: Clear all mempool frontend configs on build env reset --- production/mempool-reset-all | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/production/mempool-reset-all b/production/mempool-reset-all index 22f004610..d7e8ba249 100755 --- a/production/mempool-reset-all +++ b/production/mempool-reset-all @@ -1,3 +1,5 @@ #!/usr/bin/env zsh -rm $HOME/*/backend/mempool-config.json -rm $HOME/*/frontend/mempool-frontend-config.json +rm -f $HOME/*/backend/mempool-config.json +rm -f $HOME/*/frontend/mempool-frontend-config.json +rm -f $HOME/*/frontend/projects/mempool/mempool-frontend-config.json +exit 0 From be17e45785503c024230d1c9228986780daf681d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 8 Sep 2024 20:16:06 +0000 Subject: [PATCH 53/73] hotfix for axios breaking change to unix sockets --- backend/src/api/bitcoin/esplora-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index b4ae35da9..fc00bf2cc 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -89,7 +89,7 @@ class FailoverRouter { for (const host of this.hosts) { try { const result = await (host.socket - ? this.pollConnection.get('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) + ? this.pollConnection.get('http://localhost/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) : this.pollConnection.get(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT }) ); if (result) { From b2d4f4078f083663a1919eb4a9ea634853fd2664 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 8 Sep 2024 20:18:04 +0000 Subject: [PATCH 54/73] alternate hotfix for broken socket support (rollback axios to 1.7.2) --- backend/package-lock.json | 15 ++++++++------- backend/package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 126660166..07cc9ffb3 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,7 +13,7 @@ "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.4", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.19.2", @@ -2278,9 +2278,10 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -9439,9 +9440,9 @@ "integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ==" }, "axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", diff --git a/backend/package.json b/backend/package.json index 51abf2f7b..558a1d0b8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -42,7 +42,7 @@ "@babel/core": "^7.25.2", "@mempool/electrum-client": "1.1.9", "@types/node": "^18.15.3", - "axios": "~1.7.4", + "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", "express": "~4.19.2", From 893c3cd87d383701528f4f4400b28763dfe757ed Mon Sep 17 00:00:00 2001 From: wiz Date: Mon, 9 Sep 2024 16:53:56 +0900 Subject: [PATCH 55/73] Revert "hotfix option 1 for axios breaking change to unix sockets" --- backend/src/api/bitcoin/esplora-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index fc00bf2cc..b4ae35da9 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -89,7 +89,7 @@ class FailoverRouter { for (const host of this.hosts) { try { const result = await (host.socket - ? this.pollConnection.get('http://localhost/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) + ? this.pollConnection.get('/blocks/tip/height', { socketPath: host.host, timeout: config.ESPLORA.FALLBACK_TIMEOUT }) : this.pollConnection.get(host.host + '/blocks/tip/height', { timeout: config.ESPLORA.FALLBACK_TIMEOUT }) ); if (result) { From 3e78b636d6935cba639bf1694c8dc0e47f0768c9 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 12 Sep 2024 16:02:11 +0200 Subject: [PATCH 56/73] [accelerator] avoid duplicated accel request with double click --- .../accelerate-checkout.component.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6b1eadf7d..5c150212d 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Output() changeMode = new EventEmitter(); calculating = true; + processing = false; selectedOption: 'wait' | 'accel'; cantPayReason = ''; quoteError = ''; // error fetching estimate or initial data @@ -378,9 +379,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Account-based acceleration request */ accelerateWithMempoolAccount(): void { - if (!this.canPay || this.calculating) { + if (!this.canPay || this.calculating || this.processing) { return; } + this.processing = true; if (this.accelerationSubscription) { this.accelerationSubscription.unsubscribe(); } @@ -390,6 +392,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; @@ -397,6 +400,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('paid'); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; } }); @@ -466,10 +470,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * APPLE PAY */ async requestApplePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -494,6 +502,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { console.error(`Unable to find apple pay button id='apple-pay-button'`); // Try again setTimeout(this.requestApplePayPayment.bind(this), 500); + this.processing = false; return; } this.loadingApplePay = false; @@ -505,6 +514,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -516,6 +526,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.applePay) { @@ -526,6 +537,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -537,6 +549,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -547,6 +560,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } catch (e) { + this.processing = false; console.error(e); } } @@ -557,10 +571,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * GOOGLE PAY */ async requestGooglePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } - + + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -595,6 +613,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -606,6 +625,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.googlePay) { @@ -616,6 +636,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -627,6 +648,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -644,10 +666,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * CASHAPP */ async requestCashAppPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -678,6 +704,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.cashAppPay.addEventListener('ontokenization', event => { const { tokenResult, error } = event.detail; if (error) { + this.processing = false; this.accelerateError = error; } else if (tokenResult.status === 'OK') { this.servicesApiService.accelerateWithCashApp$( @@ -688,6 +715,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.cashAppPay) { @@ -702,6 +730,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { From 4ccd3c8525b69a406f5b81293101185989a67d34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:10:33 +0000 Subject: [PATCH 57/73] Bump serve-static and express in /backend Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `serve-static` from 1.15.0 to 1.16.2 - [Release notes](https://github.com/expressjs/serve-static/releases) - [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md) - [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2) Updates `express` from 4.19.2 to 4.21.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: serve-static dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 193 +++++++++++++++++++++----------------- backend/package.json | 2 +- 2 files changed, 107 insertions(+), 88 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 07cc9ffb3..7696eddd6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.0", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "redis": "^4.7.0", @@ -2490,9 +2490,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2502,7 +2502,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -3031,9 +3031,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3461,36 +3461,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -3603,12 +3603,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -6052,9 +6052,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6268,9 +6271,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6438,9 +6444,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6648,11 +6654,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6873,9 +6879,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -6908,6 +6914,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6919,14 +6933,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -9605,9 +9619,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -9617,7 +9631,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -9998,9 +10012,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "error-ex": { "version": "1.3.2", @@ -10305,36 +10319,36 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10436,12 +10450,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -12238,9 +12252,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -12403,9 +12417,9 @@ } }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -12522,9 +12536,9 @@ "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -12666,11 +12680,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -12804,9 +12818,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -12838,6 +12852,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12851,14 +12870,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { diff --git a/backend/package.json b/backend/package.json index 558a1d0b8..c18974021 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,7 +45,7 @@ "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.0", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "rust-gbt": "file:./rust-gbt", From 67eb815992f3b417592d9a4530ec6bd29178fc1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:10:42 +0000 Subject: [PATCH 58/73] Bump body-parser and express in /frontend Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `body-parser` from 1.20.2 to 1.20.3 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3) Updates `express` from 4.19.2 to 4.21.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 252 ++++++++++++++++++++++--------------- 1 file changed, 152 insertions(+), 100 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16400db7c..b53f80c88 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6019,9 +6019,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6031,7 +6031,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6066,11 +6066,11 @@ } }, "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -9875,36 +9875,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9923,6 +9923,14 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9940,11 +9948,11 @@ } }, "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -10177,12 +10185,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10201,6 +10209,14 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -12667,9 +12683,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -13674,9 +13693,12 @@ } }, "node_modules/object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14190,9 +14212,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -15477,9 +15499,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15618,19 +15640,27 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/server-destroy": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", @@ -15722,13 +15752,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22582,9 +22616,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -22594,7 +22628,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -22622,11 +22656,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } } } @@ -25550,36 +25584,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -25595,6 +25629,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25609,11 +25648,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "safe-buffer": { @@ -25788,12 +25827,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -25809,6 +25848,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -27601,9 +27645,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -28374,9 +28418,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "object-keys": { "version": "1.1.1", @@ -28750,9 +28794,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -29673,9 +29717,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -29796,14 +29840,21 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "server-destroy": { @@ -29879,13 +29930,14 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { From c7ab6b03fb8fcdf3c37f2025c032d3a96c2e7ccc Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 13 Sep 2024 23:23:22 +0800 Subject: [PATCH 59/73] Fix critical calculator inputmode --- .../src/app/components/calculator/calculator.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/calculator/calculator.component.html b/frontend/src/app/components/calculator/calculator.component.html index e4ade67d2..e205479ee 100644 --- a/frontend/src/app/components/calculator/calculator.component.html +++ b/frontend/src/app/components/calculator/calculator.component.html @@ -12,7 +12,7 @@
{{ currency$ | async }}
- +
@@ -20,7 +20,7 @@
BTC
- + @@ -28,7 +28,7 @@
sats
- + From a1968e01e56fc79eaa3717e139e89edd30aa317e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 13 Sep 2024 17:49:29 +0000 Subject: [PATCH 60/73] Add utxo chart to address page --- .../components/address/address.component.html | 14 + .../components/address/address.component.ts | 57 +++- .../utxo-graph/utxo-graph.component.html | 21 ++ .../utxo-graph/utxo-graph.component.scss | 59 ++++ .../utxo-graph/utxo-graph.component.ts | 285 ++++++++++++++++++ frontend/src/app/graphs/echarts.ts | 5 +- frontend/src/app/graphs/graphs.module.ts | 2 + .../src/app/interfaces/electrs.interface.ts | 7 + .../src/app/services/electrs-api.service.ts | 12 +- frontend/src/app/shared/common.utils.ts | 29 ++ 10 files changed, 483 insertions(+), 8 deletions(-) create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.html create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.scss create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.ts diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 31dff2fa5..b893d7e22 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -94,6 +94,20 @@
+ +
+
+

Unspent Outputs

+
+
+
+
+ +
+
+
+
+

diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 105863a4e..5ce82ef8c 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -2,12 +2,12 @@ import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; -import { Address, ChainStats, Transaction, Vin } from '../../interfaces/electrs.interface'; +import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; import { ApiService } from '../../services/api.service'; -import { of, merge, Subscription, Observable } from 'rxjs'; +import { of, merge, Subscription, Observable, forkJoin } from 'rxjs'; import { SeoService } from '../../services/seo.service'; import { seoDescriptionNetwork } from '../../shared/common.utils'; import { AddressInformation } from '../../interfaces/node-api.interface'; @@ -104,6 +104,7 @@ export class AddressComponent implements OnInit, OnDestroy { addressString: string; isLoadingAddress = true; transactions: Transaction[]; + utxos: Utxo[]; isLoadingTransactions = true; retryLoadMore = false; error: any; @@ -159,6 +160,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.address = null; this.isLoadingTransactions = true; this.transactions = null; + this.utxos = null; this.addressInfo = null; this.exampleChannel = null; document.body.scrollTo(0, 0); @@ -212,11 +214,19 @@ export class AddressComponent implements OnInit, OnDestroy { this.updateChainStats(); this.isLoadingAddress = false; this.isLoadingTransactions = true; - return address.is_pubkey + const utxoCount = this.chainStats.utxos + this.mempoolStats.utxos; + return forkJoin([ + address.is_pubkey ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') - : this.electrsApiService.getAddressTransactions$(address.address); + : this.electrsApiService.getAddressTransactions$(address.address), + utxoCount >= 2 && utxoCount <= 500 ? (address.is_pubkey + ? this.electrsApiService.getScriptHashUtxos$((address.address.length === 66 ? '21' : '41') + address.address + 'ac') + : this.electrsApiService.getAddressUtxos$(address.address)) : of([]) + ]); }), - switchMap((transactions) => { + switchMap(([transactions, utxos]) => { + this.utxos = utxos; + this.tempTransactions = transactions; if (transactions.length) { this.lastTransactionTxId = transactions[transactions.length - 1].txid; @@ -334,6 +344,23 @@ export class AddressComponent implements OnInit, OnDestroy { } } + // update utxos in-place + for (const vin of transaction.vin) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); + if (utxoIndex !== -1) { + this.utxos.splice(utxoIndex, 1); + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + this.utxos.push({ + txid: transaction.txid, + vout: index, + value: vout.value, + status: JSON.parse(JSON.stringify(transaction.status)), + }); + } + } return true; } @@ -346,6 +373,26 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions.splice(index, 1); this.transactions = this.transactions.slice(); + // update utxos in-place + for (const vin of transaction.vin) { + if (vin.prevout?.scriptpubkey_address === this.address.address) { + this.utxos.push({ + txid: vin.txid, + vout: vin.vout, + value: vin.prevout.value, + status: { confirmed: true }, // Assuming the input was confirmed + }); + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); + if (utxoIndex !== -1) { + this.utxos.splice(utxoIndex, 1); + } + } + } + return true; } diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.html b/frontend/src/app/components/utxo-graph/utxo-graph.component.html new file mode 100644 index 000000000..462e4328e --- /dev/null +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.html @@ -0,0 +1,21 @@ + + +
+ +
+
+
+
+
+
+ +
+

{{ error }}

+
+
+ +
+
+
+
diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.scss b/frontend/src/app/components/utxo-graph/utxo-graph.component.scss new file mode 100644 index 000000000..1b5e0320d --- /dev/null +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.scss @@ -0,0 +1,59 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } + @media (min-width: 992px) { + height: 40px; + } +} + +.main-title { + position: relative; + color: var(--fg); + opacity: var(--opacity); + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + display: flex; + flex-direction: column; + padding: 0px; + width: 100%; + height: 400px; +} + +.error-wrapper { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + + font-size: 15px; + color: grey; + font-weight: bold; +} + +.chart { + display: flex; + flex: 1; + width: 100%; + padding-right: 10px; +} +.chart-widget { + width: 100%; + height: 100%; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} \ No newline at end of file diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts new file mode 100644 index 000000000..5e034a700 --- /dev/null +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -0,0 +1,285 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { EChartsOption } from '../../graphs/echarts'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { Utxo } from '../../interfaces/electrs.interface'; +import { StateService } from '../../services/state.service'; +import { Router } from '@angular/router'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { renderSats } from '../../shared/common.utils'; + +@Component({ + selector: 'app-utxo-graph', + templateUrl: './utxo-graph.component.html', + styleUrls: ['./utxo-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 99; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class UtxoGraphComponent implements OnChanges, OnDestroy { + @Input() utxos: Utxo[]; + @Input() height: number = 200; + @Input() right: number | string = 10; + @Input() left: number | string = 70; + @Input() widget: boolean = false; + + subscription: Subscription; + redraw$: BehaviorSubject = new BehaviorSubject(false); + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + error: any; + isLoading = true; + chartInstance: any = undefined; + + constructor( + public stateService: StateService, + private cd: ChangeDetectorRef, + private zone: NgZone, + private router: Router, + private relativeUrlPipe: RelativeUrlPipe, + ) {} + + ngOnChanges(changes: SimpleChanges): void { + this.isLoading = true; + if (!this.utxos) { + return; + } + if (changes.utxos) { + this.prepareChartOptions(this.utxos); + } + } + + prepareChartOptions(utxos: Utxo[]) { + if (!utxos || utxos.length === 0) { + return; + } + + this.isLoading = false; + + // Helper functions + const distance = (x1: number, y1: number, x2: number, y2: number): number => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + const intersectionPoints = (x1: number, y1: number, r1: number, x2: number, y2: number, r2: number): [number, number][] => { + const d = distance(x1, y1, x2, y2); + const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d); + const h = Math.sqrt(r1 * r1 - a * a); + const x3 = x1 + a * (x2 - x1) / d; + const y3 = y1 + a * (y2 - y1) / d; + return [ + [x3 + h * (y2 - y1) / d, y3 - h * (x2 - x1) / d], + [x3 - h * (y2 - y1) / d, y3 + h * (x2 - x1) / d] + ]; + }; + + // Naive algorithm to pack circles as tightly as possible without overlaps + const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = []; + // Pack in descending order of value, and limit to the top 500 to preserve performance + const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500); + let centerOfMass = { x: 0, y: 0 }; + let weightOfMass = 0; + sortedUtxos.forEach((utxo, index) => { + // area proportional to value + const r = Math.sqrt(utxo.value); + + // special cases for the first two utxos + if (index === 0) { + placedCircles.push({ x: 0, y: 0, r, utxo, distances: [0] }); + return; + } + if (index === 1) { + const c = placedCircles[0]; + placedCircles.push({ x: c.r + r, y: 0, r, utxo, distances: [c.r + r, 0] }); + c.distances.push(c.r + r); + return; + } + + // The best position will be touching two other circles + // generate a list of candidate points by finding all such positions + // where the circle can be placed without overlapping other circles + const candidates: [number, number, number[]][] = []; + const numCircles = placedCircles.length; + for (let i = 0; i < numCircles; i++) { + for (let j = i + 1; j < numCircles; j++) { + const c1 = placedCircles[i]; + const c2 = placedCircles[j]; + if (c1.distances[j] > (c1.r + c2.r + r + r)) { + // too far apart for new circle to touch both + continue; + } + const points = intersectionPoints(c1.x, c1.y, c1.r + r, c2.x, c2.y, c2.r + r); + points.forEach(([x, y]) => { + const distances: number[] = []; + let valid = true; + for (let k = 0; k < numCircles; k++) { + const c = placedCircles[k]; + const d = distance(x, y, c.x, c.y); + if (k !== i && k !== j && d < (r + c.r)) { + valid = false; + break; + } else { + distances.push(d); + } + } + if (valid) { + candidates.push([x, y, distances]); + } + }); + } + } + + // Pick the candidate closest to the center of mass + const [x, y, distances] = candidates.length ? candidates.reduce((closest, candidate) => + distance(candidate[0], candidate[1], centerOfMass[0], centerOfMass[1]) < + distance(closest[0], closest[1], centerOfMass[0], centerOfMass[1]) + ? candidate + : closest + ) : [0, 0, []]; + + placedCircles.push({ x, y, r, utxo, distances }); + for (let i = 0; i < distances.length; i++) { + placedCircles[i].distances.push(distances[i]); + } + distances.push(0); + + // Update center of mass + centerOfMass = { + x: (centerOfMass.x * weightOfMass + x) / (weightOfMass + r), + y: (centerOfMass.y * weightOfMass + y) / (weightOfMass + r), + }; + weightOfMass += r; + }); + + // Precompute the bounding box of the graph + const minX = Math.min(...placedCircles.map(d => d.x - d.r)); + const maxX = Math.max(...placedCircles.map(d => d.x + d.r)); + const minY = Math.min(...placedCircles.map(d => d.y - d.r)); + const maxY = Math.max(...placedCircles.map(d => d.y + d.r)); + const width = maxX - minX; + const height = maxY - minY; + + const data = placedCircles.map((circle, index) => [ + circle.utxo, + index, + circle.x, + circle.y, + circle.r + ]); + + this.chartOptions = { + series: [{ + type: 'custom', + coordinateSystem: undefined, + data, + renderItem: (params, api) => { + const idx = params.dataIndex; + const datum = data[idx]; + const utxo = datum[0] as Utxo; + const chartWidth = api.getWidth(); + const chartHeight = api.getHeight(); + const scale = Math.min(chartWidth / width, chartHeight / height); + const scaledWidth = width * scale; + const scaledHeight = height * scale; + const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale; + const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale; + const x = datum[2] as number; + const y = datum[3] as number; + const r = datum[4] as number; + if (r * scale < 3) { + // skip items too small to render cleanly + return; + } + const valueStr = renderSats(utxo.value, this.stateService.network); + const elements: any[] = [ + { + type: 'circle', + autoBatch: true, + shape: { + cx: (x * scale) + offsetX, + cy: (y * scale) + offsetY, + r: (r * scale) - 1, + }, + style: { + fill: '#5470c6', + } + }, + ]; + const labelFontSize = Math.min(36, r * scale * 0.25); + if (labelFontSize > 8) { + elements.push({ + type: 'text', + x: (x * scale) + offsetX, + y: (y * scale) + offsetY, + style: { + text: valueStr, + fontSize: labelFontSize, + fill: '#fff', + align: 'center', + verticalAlign: 'middle', + }, + }); + } + return { + type: 'group', + children: elements, + }; + } + }], + tooltip: { + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: 'var(--tooltip-grey)', + align: 'left', + }, + borderColor: '#000', + formatter: (params: any): string => { + const utxo = params.data[0] as Utxo; + const valueStr = renderSats(utxo.value, this.stateService.network); + return ` + ${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout} +
+ ${valueStr}`; + }, + } + }; + + this.cd.markForCheck(); + } + + onChartClick(e): void { + if (e.data?.[0]?.txid) { + this.zone.run(() => { + const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`); + if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { + window.open(url + '?mode=details#vout=' + e.data[0].vout); + } else { + this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` }); + } + }); + } + } + + onChartInit(ec): void { + this.chartInstance = ec; + this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); + } + + ngOnDestroy(): void { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + isMobile(): boolean { + return (window.innerWidth <= 767.98); + } +} diff --git a/frontend/src/app/graphs/echarts.ts b/frontend/src/app/graphs/echarts.ts index 74fec1e71..67ed7e3b8 100644 --- a/frontend/src/app/graphs/echarts.ts +++ b/frontend/src/app/graphs/echarts.ts @@ -1,6 +1,6 @@ // Import tree-shakeable echarts import * as echarts from 'echarts/core'; -import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart } from 'echarts/charts'; +import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts'; import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components'; import { SVGRenderer, CanvasRenderer } from 'echarts/renderers'; // Typescript interfaces @@ -12,6 +12,7 @@ echarts.use([ TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, - LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart + LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, + CustomChart, ]); export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption }; \ No newline at end of file diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index de048fd2d..ee51069c5 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; import { AddressComponent } from '../components/address/address.component'; import { AddressGraphComponent } from '../components/address-graph/address-graph.component'; +import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component'; import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component'; import { CommonModule } from '@angular/common'; @@ -76,6 +77,7 @@ import { CommonModule } from '@angular/common'; HashrateChartPoolsComponent, BlockHealthGraphComponent, AddressGraphComponent, + UtxoGraphComponent, ActiveAccelerationBox, ], imports: [ diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index b32a2aae6..5bc5bfc1d 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -233,3 +233,10 @@ interface AssetStats { peg_out_amount: number; burn_count: number; } + +export interface Utxo { + txid: string; + vout: number; + value: number; + status: Status; +} \ No newline at end of file diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index 7faaea87c..8e991782b 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs'; -import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary } from '../interfaces/electrs.interface'; +import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '../interfaces/electrs.interface'; import { StateService } from './state.service'; import { BlockExtended } from '../interfaces/node-api.interface'; import { calcScriptHash$ } from '../bitcoin.utils'; @@ -166,6 +166,16 @@ export class ElectrsApiService { ); } + getAddressUtxos$(address: string): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/utxo'); + } + + getScriptHashUtxos$(script: string): Observable { + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/utxo')), + ); + } + getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 8c69c2319..6bdc3262b 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -1,5 +1,7 @@ import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface"; import { TransactionStripped } from "../interfaces/node-api.interface"; +import { AmountShortenerPipe } from "./pipes/amount-shortener.pipe"; +const amountShortenerPipe = new AmountShortenerPipe(); export function isMobile(): boolean { return (window.innerWidth <= 767.98); @@ -184,6 +186,33 @@ export function uncompressDeltaChange(block: number, delta: MempoolBlockDeltaCom }; } +export function renderSats(value: number, network: string, mode: 'sats' | 'btc' | 'auto' = 'auto'): string { + let prefix = ''; + switch (network) { + case 'liquid': + prefix = 'L'; + break; + case 'liquidtestnet': + prefix = 'tL'; + break; + case 'testnet': + case 'testnet4': + prefix = 't'; + break; + case 'signet': + prefix = 's'; + break; + } + if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) { + return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`; + } else { + if (prefix.length) { + prefix += '-'; + } + return `${amountShortenerPipe.transform(value)} ${prefix}sats`; + } +} + export function insecureRandomUUID(): string { const hexDigits = '0123456789abcdef'; const uuidLengths = [8, 4, 4, 4, 12]; From a76d6c2949cb1e59741bb8ee5f6572626f4c8f0f Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 17 Sep 2024 14:47:42 +0200 Subject: [PATCH 61/73] Fix mobile routing to tx push and test pages --- frontend/src/app/route-guards.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/route-guards.ts b/frontend/src/app/route-guards.ts index 4808713c1..7ed44176a 100644 --- a/frontend/src/app/route-guards.ts +++ b/frontend/src/app/route-guards.ts @@ -13,7 +13,8 @@ class GuardService { trackerGuard(route: Route, segments: UrlSegment[]): boolean { const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode; - return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98; + const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments; + return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path)); } } From 2d9709a42707903d4667eacdbf2e0ed311dc0e2b Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 17 Sep 2024 12:15:18 +0200 Subject: [PATCH 62/73] Pizza tracker: hide ETA on replaced tx --- .../components/tracker/tracker.component.html | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index d467aae80..252c1189e 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -65,23 +65,25 @@ }

-
-
ETA
-
- - - @if (eta.blocks >= 7) { - Not any time soon - } @else { - - } - - - - - -
-
+ @if (!replaced) { +
+
ETA
+
+ + + @if (eta.blocks >= 7) { + Not any time soon + } @else { + + } + + + + + +
+
+ } } @else if (tx && tx.status?.confirmed) {
Confirmed at
From 99290a7946b96a11dcf519ddcafee4a777d9d782 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 17 Sep 2024 14:34:18 +0200 Subject: [PATCH 63/73] Show http error in pizza tracker --- .../src/app/components/tracker/tracker.component.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index d467aae80..7cb100cf7 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -42,7 +42,7 @@
-
+
@if (replaced) {
-
+
@if (isLoading) {
@@ -184,6 +184,12 @@
}
+ +
+ + Error loading transaction data. + +