From 104c7f428534971f294c623042d59d49f3b7ba4b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 8 Aug 2024 13:12:31 +0000 Subject: [PATCH 001/102] 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 002/102] 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 fbf27560b3eca06504994cca534799f27dd9f561 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 10 Aug 2024 13:53:49 +0000 Subject: [PATCH 003/102] optimize processNewBlocks --- backend/src/api/mempool-blocks.ts | 92 +++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 5d9dcf8f4..6e547e653 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -369,7 +369,7 @@ class MempoolBlocks { const lastBlockIndex = blocks.length - 1; let hasBlockStack = blocks.length >= 8; let stackWeight; - let feeStatsCalculator: OnlineFeeStatsCalculator | void; + let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; if (hasBlockStack) { if (blockWeights && blockWeights[7] !== null) { stackWeight = blockWeights[7]; @@ -380,28 +380,36 @@ class MempoolBlocks { feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); } + const ancestors: Ancestor[] = []; + const descendants: Ancestor[] = []; + let ancestor: MempoolTransactionExtended for (const cluster of clusters) { for (const memberTxid of cluster) { const mempoolTx = mempool[memberTxid]; if (mempoolTx) { - const ancestors: Ancestor[] = []; - const descendants: Ancestor[] = []; + // ugly micro-optimization to avoid allocating new arrays + ancestors.length = 0; + descendants.length = 0; let matched = false; cluster.forEach(txid => { + ancestor = mempool[txid]; if (txid === memberTxid) { matched = true; } else { - if (!mempool[txid]) { + if (!ancestor) { console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); + return; } const relative = { txid: txid, - fee: mempool[txid].fee, - weight: (mempool[txid].adjustedVsize * 4), + fee: ancestor.fee, + weight: (ancestor.adjustedVsize * 4), }; if (matched) { descendants.push(relative); - mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0); + if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { + mempoolTx.lastBoosted = ancestor.firstSeen; + } } else { ancestors.push(relative); } @@ -410,7 +418,20 @@ class MempoolBlocks { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { mempoolTx.cpfpDirty = true; } - Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); + // ugly micro-optimization to avoid allocating new arrays or objects + if (mempoolTx.ancestors) { + mempoolTx.ancestors.length = 0; + } else { + mempoolTx.ancestors = []; + } + if (mempoolTx.descendants) { + mempoolTx.descendants.length = 0; + } else { + mempoolTx.descendants = []; + } + mempoolTx.ancestors.push(...ancestors); + mempoolTx.descendants.push(...descendants); + mempoolTx.cpfpChecked = true; } } } @@ -420,7 +441,10 @@ class MempoolBlocks { const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; // update this thread's mempool with the results let mempoolTx: MempoolTransactionExtended; - const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => { + let acceleration: Acceleration; + const mempoolBlocks: MempoolBlockWithTransactions[] = []; + for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { + const block = blocks[blockIndex]; let totalSize = 0; let totalVsize = 0; let totalWeight = 0; @@ -436,7 +460,8 @@ class MempoolBlocks { } } - for (const txid of block) { + for (let i = 0; i < block.length; i++) { + const txid = block[i]; if (txid) { mempoolTx = mempool[txid]; // save position in projected blocks @@ -445,30 +470,37 @@ class MempoolBlocks { vsize: totalVsize + (mempoolTx.vsize / 2), }; - const acceleration = accelerations[txid]; - if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { - if (!mempoolTx.acceleration) { - mempoolTx.cpfpDirty = true; - } - mempoolTx.acceleration = true; - mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; - mempoolTx.acceleratedAt = acceleration?.added; - mempoolTx.feeDelta = acceleration?.feeDelta; - for (const ancestor of mempoolTx.ancestors || []) { - if (!mempool[ancestor.txid].acceleration) { - mempool[ancestor.txid].cpfpDirty = true; + if (txid in accelerations) { + acceleration = accelerations[txid]; + if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (!mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } + mempoolTx.acceleration = true; + mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; + mempoolTx.acceleratedAt = acceleration?.added; + mempoolTx.feeDelta = acceleration?.feeDelta; + for (const ancestor of mempoolTx.ancestors || []) { + if (!mempool[ancestor.txid].acceleration) { + mempool[ancestor.txid].cpfpDirty = true; + } + mempool[ancestor.txid].acceleration = true; + mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; + mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; + mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; + isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; + } + } else { + if (mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - mempool[ancestor.txid].acceleration = true; - mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; - mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; - mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; - isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; } } else { if (mempoolTx.acceleration) { mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - delete mempoolTx.acceleration; } // online calculation of stack-of-blocks fee stats @@ -486,7 +518,7 @@ class MempoolBlocks { } } } - return this.dataToMempoolBlocks( + mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( block, transactions, totalSize, @@ -494,7 +526,7 @@ class MempoolBlocks { totalFees, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, ); - }); + }; if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); From a31729b8b8ffaf611cd2274954d7abd2cc419e79 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 10 Aug 2024 21:56:11 +0000 Subject: [PATCH 004/102] 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 26c03eee88101904e242b049153f6a47fccd8215 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 14 Aug 2024 14:21:47 +0000 Subject: [PATCH 005/102] 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 006/102] 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 007/102] 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 008/102] 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 009/102] 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 010/102] 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 011/102] 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 012/102] 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 013/102] 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 014/102] 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 zcmZ5{1z42J_pnH*w4ih=Aq^7J$O0;|lyphw5)w--sdR`aU4n#kcS)^CcP=T-g2XPu z65q?czw5pKZ=T1;hj(YroO9;P=?PZ@D-qqHxpU*j4Wj4IKpHo0;1=Atfn{+E5BSn8 zHy;iBciZ`yp6iVp1mxHMux_NJQ2`&ZTs4%Q-Y6ZU+XDW;v6fSjyK$o;mf+kJ_r{IE zq~{>HSDskA7e4PF>i9AKhN)^}BW_p)*uBn38)1^u|4>tjPegq0utz*>H%x^xecAT?H-tVC9ISXhj2w6#TW#pta@xVcNhRnV;uVlw>jG9c=Gz z>NG_lqVL@ubzYFAT(~Hd^cfs*74&D{FM(z>pZ@kErPPA$Ech-qCoi6_J-I>lXDH;> zw3epUgkU|B1^pS-ID`e^VgC#pPH;fM#-Ncu1jPnU$2x9&dbypMadE6b`VSQKjf$WY z?;XmrtJjChQ*HjI_ra}J9a}r=U&Cj2D{`h+Jq7&;r1XLq)5 z{)^rim73;;?IxGl0|R%+TnXO4DBTB8YQ{OEi2;=b|Cuiyi6!j5kePOIoNy}8&CI#f z->_3g|M|l14-$6R*QAh(w7r$D9Ppj*=oS8#YwQA9dB$Sl2B}J0+#_$tRRjNp8;&K6 z>ZqxTO?DRdU(PD4Gp+S96lycN`7b`Bw@FcNdm?V9q29raMQR2Drz3V)#{aM=yVOip zrR4xom6>+Ag4(xUpL!+jRFo@xe2*!4b}Hb{PTqmQPEmf}K%+Z*jGA+A;hBOT{{Y#3 z2yluk-x+h7S`S6Xt9bsw{SO{wWJo!Cn31SG5%&9kp@!MqrcxvSGigMF|KG!7$Geug4f$n8x5Ir`FMQ&49JKzG$}lv5 zLrj;ps{95e<$15RmdS-`>mhn##{ORz9@kSI48`yGTo}oh%}kZ2{EM|mXFv)uvaa=T zuo$t0-CSLecbV^GVlDhHB@@6=%X0>;`>v8Y1x|XeNT}odp)&4zwZhJ)BC$fr#or}m z%_y3puYa!Jci)ruCx*o`#uha##*gZd}PZ$lbz-+UZKXQ|L>0(BBcwukYZaBCQR_6 zWDC|Gpe+1Zc`yg3p`bOzf(J4L|9%g}5=L&FZh)Pxau$6z1vby091TI};-31wQ}Ow8 zWN}>pu|G*JSI36fpI_oUHg0F0$&=b^Vx(pI1L-a}U^BoF_juOHf34>gtKO8X_xlsh zpNS%anbL1i08{B>C&lkweQa}T!vCm2Z9|-hC{IQ;+4&VW`u|ug>A#HKf&Q+ir$fjk zq*p)^7>982>|*zO`!9Z;G4eC8N8?WkcTc`L>Ub>Jb}>O~+ZykX#Iu>)Vw|5Pay8;4 z?a2TFNv(kE`LgwrpV-WSa2&lgkHpLwVw)B(%u%GlmH3Itp3PBajdlELjx*^lu}bPZ zaPQ$t#vnE5Hv|BZQe{Lk%dJG;5+P$X3xM3r29 zu|ED_YPk686K9(*Rk|*fzdmA~$oXng1lFX-a(e&ZX>#QtZblAK99VJ?_nSPeI2TlV zY(WbgnFa>ADc4RoHsKcZc$!bAj4V8l;HU2Ib7YwzNy~Okzk~VsaO-{<}v$gf$ zg6TY}c4)s}e_b=e9Gpa3CZnVS4Ge`>h^1woIeik+OVK7&D{W`9_9TJA?ry9a9*IRWnUID?YN& zN722}d}JDVi!;MbCh>y0k`h+>`4(51sca*S8g!ScGo{3zzt}+mj!sXI0;gpEW}vqW zpjJlS{E5?-Xo>@W=}uTcX)dc0T`}OdDcP`B__gkGNtDt1CjGCX#by`9ho0MLPQw}B z6o@JL$YmpiS`&6JfldC+UmSu*o(u}_-+yMcW1k~;o1*BJJ7wEc>v|U&xw&rL`9rOE zRau@uuko&0(@-NqY4&w)^oyaKX%N0@E8-;bg3z7$E7A&OkzS_wWRcECsia@?e zx58o1k<}+^d~IKiODI-(sXKyor}yN`C)2FIPz~2VG`O0Gjua{G`5=WUp^Z1W$Pq2O z+7?|LMjzNO`kG^dK=`kN`4G)Jp=lw)7zPBNj{9`ri=F=8LrHMUqx#R7HDRObEoBUU zpKUhgWTw1y2dl)K-kSa;v4CGw=H8t3g>cJHNPCq3gd}eYoU75lbo-${oGvl>k_gdE z5o)pE3O;d?IZNSi2?Z=5O!`#fzgezS~Fp=OMNXG9CvXsYNTi5a<6N%@#^X_g0Oq-gU*Krv*y z^ev$q6ZVtV0#v{ka373w>E2F^KY08~QEg z-myeltp)|=dlAHU5aD6P*XWpawOn0e#ZXp{3?$B~WmFk);*5Ztiq{2RY-MlH`(1`Q z`);YI`NqydvielCtL*~d%G!4rgre~PAA-k#=fZxmvEuTi^|U8CU9v3(dw#E~s*GHuV#_;cqiz=vI60L)pBmwIW_ei9U?249a<{I z_U#hpmG0)*cSFoPqJ4uyczo+fNIRTLJHn1u7aJE(`*(Tj?TARNoJ$6GI3S*RI~o-5 zmu1eGGKRoCg*wz6m%)w(C>LQe{*YL^t?qtGl+*bEk+J2&Y%uBPJ_W)lnur@#K_Q#j z>08VpT9FR*cRo|FnypWXD7>CJctW^Hqv$7KCR*dn)6JjD0dC~WVa6tT2yB8iLBqlS z!JLo_mk<3(5q)|x$v@iksOw-9XZ3Xz9fJ@N#ow;s#mS|KlVSCM=d_(I5e!T(@Uh;9 zow2i*m&`p)B8%?LCQGi(rnaFAlXPBWG&w~dUw3=>Bc{2S(_ud8aY(sf!Ae3f;(iE2 zsTXBx0UpEho2uQ`l}Ff-KfcEZGz(XcH|y#|Z)SI~lsXCgg%!zCQvN zvs3Z;EL)CaMLw*3SAjq!tWZ(r^kXuw%pEJlv7tliUZ)g>owo~j`Hm-YpQz`CU&*HP58)spOOWQ3#!l5h8&2y1uP}Z z>8AKhM_%yyj=WO~UIFv&S_f0UFAA{y$ZqI}VK}ONzjBMYX3kjT+t6@I6S=8dU*O^= zELw2PN(_bibBUh9EyVQD)f!tWc>CsvZ=Y^a9@Kn@$Jj#H{R%KJgPv#uQH{B+&m{?X z<64iWjcwCIyzhWO$QSBwc^&ZEZjlmGXzXM|J+yJ&iG)&UG(k+UUy1hEsWDc@7)8jx z9X}@=s92=sGXr(u{-XGq%OR~4EVXwR{)~K>g)(&1ZSq;FlQ^M|>E0RXBq|BmFc&l7 zv>>)xm`k#_wXB}}3p3+?8Gk!0Y&JfKJ!W*)?_M@x{>icLqH3ej!{O* zQ8QN@N)G&YKv>g(nCm^#z3Gwg3^ab5GvfX1rG-yKD6~Qef2sIPlA&prH@L?JvBzqJ z%OTM#PDlGIs-#nW)`(A@Ys%sq+h)Buqehi(jyfOutDWEOHEs?WGA%^8c{a2X$`4q! z@A#~AdpwTavzy|wH!ANb99pGSZfc}Bgfcn=#B_M1)Z=A8Ei2qcR3EY^yK1-{$2_=i z|9DXJBrZsNA16%w>He;v#&bcrzQa$S2-e{gAW*eKv9nksg@V=^p7{4Ps_Fe%A~$&} z27~p3Moa^eK@aKv^|vi!Y|gZzCU<6wij?MymK!wj6mE32L@V~|u2oU3P9bdaHCMj$ z6Mo~$h#@7!zQ^KBF8{F3i1Y{wDTk9_r0;hrG3K8*In`FT4Y-4uMzh2k6z|BaM|h05 zCg1Oz@tk0ccN!xzPbs8Mn`6^&s)wEf2Dd&C4Yinq?x`}&`JO$7YJ(oHg7J*y+Z-C; zMFqqUOo_WH75F@kY#1-!E@gBDN|p)1L@(JkW6w6`uY4S3 zwxm~4LtcrzYTWW6*;%XZZ2GS zhZT5PD4k}wWO6m^8=|3kmTlyK2ZGoC#s?Q6)@t+9XI!}MHDm49f!hiG^QXDjb!<%PWD%%jbKTX40Ffo4ed1|SFd=PgdH6`vEFhpl?NwrJZ9 zzvG344W|aoGRV$u_i{$Wzf*kr9J*2-veL!e?X04nZlf}CqYm-1f0de>FHFzwx5mcJ z+dgm7vtgPP>HMPTQ{Ao_0=ucxC)G~2%P&-KZgYm5K5+^r`Gq{`ufc0GXX9u!Pq?Se zje2Svd()0lV+OXV9kupQTG_+vi*Ck7|G88w@tc7skk?PnUDk!~Rd9k&=(pRH64#3L z6x?sSZS|)+f-kj`Mt_v58@Gp{m2wNF-(0qZO5*t!a7FsV zN8D@MYmb2ImmQOuUGOVO%d6UjX{*P)NC=5^ratTA<)Yo5i;st~hVI+`_a0h4XS6{q)3^+HVb?Buq8#+O50k^6yxt|Y@hlD9U-7R4ZFj#q8G=Tl9 z^erC*RyK<|n-`Q}9sMxrmEZB{;dIvraCOPUlA}ECi=)-TRtFAY1qpFi<}%jZhN)3$ znbEs4%GQSNK04J#db2f6_2iZ|usQj9H~*8lxe;31T;f zNHZ^Z4La{OcnE2B)xBF0I+PU8TZ=CZFVbZMq8QxfBYKIE!C?~g%>{F#-{{2CASf+ZXABsqz*D+f_xuFK(dgqAorR9f^$b7`yI37W~$`R z@(H^AfLgp(+8+i{%DO>5i$i)Uav-AvLnT!zhxewcG!nu_!qgGNhcx0{>7>(^Q;HIa z$^<4Li4PSsOWCgM`X9u$>{fAnU={=Sr-#(p3)gcKWH#U~y+-Xjv(SFL+0*7U7sz?c z1O@&1`Lj~$0AUKsp1El-^0JBmno~V--cCWkh{s*0T0SvOy<(5qi>T#Yx8B_B_I+;n z6S9h_;>v{Hy|w{}mUsuV+smOMI-|0nYD14jXl);UT;b-kOvEif)g!K9gP}R zM3uHB%|%9@YNSFl1I2@Z(@GdT5^)+>5Lp$HXisNvwVG>ZW6_VvH*$Z&SKkhaaQr@; z6YLDlz9+F{qA0{zL2~Rv4ff%silWx2gZaFf%iH@>Lpi@8iB<^j+*Ao} z%GkkN3d#FGnN)@=6AJS&twh!C&>#B!kL>DTDx_02(g@qyXUde1^b`=x02`|Gf%GGx ziM`}W`$ez0l`pOTZ!X3i6@CyX*!3&Pr!j)*UzRiYZPebn%qv0JoGZ!PpQ|#3yIgaJ z*e5d#_63T9LS_y1LLA?<;IE)}bjp1bz3+oD{EyJ$nXln2X%l0VcF`k)f((Wg5z?lw)iqRYVaez3Z+DbzCl|ifi84!)KQeIJ~9x`PEhXSUx7nsSDPH<=P&CUO` zA++~^YyB)U3zz4W>PaP0H>}H{^|g)Gc%nBQ7!%QEY4p8s`07Pt(pXzRvE|(R&rRtx zjLG)IJRU-N5aZ2ASRgcPPB!HCoNSbuj7QDh80@mu@Jnv1L5TN>UKx;V16QR#+%{(z zPNko%(E|bi$#a~d-0*^6Z~A6kWkM4{!kj`6@`GwEdNDc4ma8c4yA7v7&->9r1_?yV zwQk(|PZvI6`NA#23>08^of8`pQRts%-|9m=Gi+B^-WAQiBpYICljoUoQ14$IQk3tz z&%182t!W|>I`UKw-azIii0}f0v&TC_uK8BG2oFo%F4~}^Z(VBG+s2Eg zZn6THZ{>^W_F?D8Nvyr zX3gfnF?!51wYB??sYd~cICxqB7m%b*a9ys~+M8xwS18UWH7U*m2@={cmZ#pY4P;CG z88#Eur57dNzuF;i{CwCJU;*Xv;GnTdsb$S^@tC}}GP$}8Ad6FwS z5~n8uD;|OKSPL#AXFy=_0I)wE>PakJ<>Z0H`Vt#e#y622wT;>%&XurP#fBX^fyrMc zOCrNzihH0)Ujctt(2lr#b7addw@WXeE&1w)pGanj;-9|Q$_L24<~IY4nU=D_rl-9XPsKN}XEsJrQwsUDKE0|cIriaob;9Mu!2ksp5J!VgK!uhLAX;mJPX?!KPI+tyGlhafRUs6fXIhz=(0xGe=l@3g5Ks!mn< zDPeeK-$x6iD_$0PVfO)6ro=j;?M zsErlq`+KQ>Zt>9b;{S27+K{`dzd2dI8t(*U) zfMW%kUf^aCoD8nEFDOxf?*6FzxEd6LrX z$8mRaU|K>ieJT^Oz2r^cJ~N;rvv*7aDgq7_g#jg>byR!Ki0Pl=qHKyd->Gzc&5EvI zOwaO?Z|&R7$_q0O%8AsHlUgS@(=PVA&6f3dfo8l^WY)%*hBB&)P$`e9RvK5Cb0x{_ z%lw9QgcYg;fXieVPjgBAvl(RM$QkRL@G8KtaT$$Y4fa2KediDn_| z^j`}nYpm&3eSYUwh^(zNoW@oRTPEF2VkKgaA5x=g|OJnaA{3MEtw^TA~Z4Ei<UG6=zB(c-*$qU>m%*xN>Xv z@YzT)+Z<=?!9_2my`q~b?r9%ID75qd|dW*zlFlZf{C*Sw<+h9e7+5X%>HL8_#kHi zkQN7ORkK1Ncb06Z4BcPc*^X~DR9lih^sGPc;l!SBZ^#z>ahXKPx!$GA5ULL?)t3$- ze4sFc9yn8$D*@C12sEBS28Ls$?R+vk)v-PUbJ-NTAKVHvjcp3=u$tIySh z4`j%|S)lb7js+y1{0lsU1)O+(gW<&ccm+Hvz9+d^(gx@QRXqI%@<*Pf5Xene z(q^BrR4CgtVuX60)WXJC=Om{%yKL)O+e6NRAY<5 zZs!n=`~0vQRkITaMTL}kN6(YPU5_(N*1F7IIIBnu6t`Yj`l6i4DRJP%5-FaC$B0sB5 zZR?A6ZL7|h&lz&Oh}^fjF2e!UXEJrsG(4WWtXaF;P)6)+FHRP<7xY%N-$?^Yh-H4d z5$bo@dGW3$RD_e*jVHIEPKDIGu{+;yOKe}4T?=CHksuJ_ml(&=&7vOpqL7pqW?B%5 z5Z}Nc$_Gz5SC%Rq`vCivb7%;ZUmo*6ab2fty8*0wDrv*cvW=Hf)UQ0!oY#6H2zveW zl;T0U+)-s`vzXZ;iNJcvQ1&kN3{(j--Gwg<@x*_Y?>OtV5-vtYrfx~#Vu;=58XiV$ zJ`oa6sCkn!CGj&g^@xLJ4~`4 zKS-f0A`M2jvq=}I_afp`@7VD(TIHf10_9HicK5;f&tA=tPhp$C*qAsKU%P*Fej`eq zB8oLoFY-Ncp(}Lkr^l*_tVYCo3$7hKww>MhwlBP z1a5n#f~W1Ft0Ed{JI5kpUW-URak}8d1xf50d&9sKAtF*$pe)&g;=?a*5-f^+Wn*2-ev1I2Yu@GmxCMTn3 zI7xp8Xmah&iLHv8+asX~M)rryW|{OG)BW&%oPoZ6?>^ZQpxy>h4hmf-UND~rUlOV| zhJ_KAY_YL==kp;)6bX6Q9jT-AiV1}IRySjg77$UFrMIg}=78Ljm%waTKKB1Eo071? z(eCpp91zHjs;Km0O~62jPT*!wA}O4v2YGWPX=T$)s~uGX~{Zq*&|Essb6#C5V~Qr);~+qNhikl2c+ed9dw0^!s@ zR9$>4_Qa}|kEaU@Ai2H~VrG5|wv2#I(b+rw_2>gGb2V&lAynpDA)p4c(GZr5B6EmI zT-YX6so8^hD5%(3P{L}i6Eg#nOoSg^*shOhkCigV*pjO>*CTCcU5aDZvOlH{5p|Ja zj=-2}>fanLUj$p5XkE1D_yhGy5QWZ9D`}0W`mZE&3EV~B!#8jBxZy`YMjt59X@V#k zl4Fy;!KMrqB~`S?jpZcG8#pFVFTBL@u021l(idl0dk$BC41mTXbn7?`DB)D2jgLO> ztRJjQJ5I4@D-vE3fBZi2QM+Gur0K0E-W7o15FX%#mQ#|ZIHa&@3o$0Na0qYaNgGxP z+lR5QTgP4Zm7qe9bO%|7bD}PolFT4LiRo8_d)Afe>%b{SBb>b_q)LUpuw~1?Agil% z`)c^xkN&`cGq;vh?`Ixk@i*gwaws+P9&Utv>5mpNN1qfHAVNSbuXEX<+{jfLh7SN0 zweUWjb#H^X(-vUrWIcI((kCmhd`|wq8j>{29B?$<=cv@eH-7tKLl7<#;zn zyW~>6J|fv`I#lSomy<1G^{KQiORCEpw+WEb&OtJXYrH!g0l4eIj8_uZ+Nvl+$*@+# zu6!aOLE`H?@DFEPU$G zh`EUq@!Jv`;BnweoAX0vQcug%Ssa;4wTn0n>Ir&hsnnj)OO#ieX^X=hcRcywcX$k= zDyPwIN&ruc%kPcUEu1qS4rSi&=U9XgRFlUa(Ey z!pke6F#kGmNVdtl+V0ZqT5m>;ru{47E@AgigX;Bm2tpUsrS3PCE4&pmlw0UE`03Ln zqvn&dwjQ)U<|_T7T5B|N`iCv81z^S1G4sfBNcEhp{I*Vr8ky&J=h-=DIU=$VpknuQ zd-_wpdi{?rm9CQxL2+gKuMxA`md_yjf`TM-#c-MxAL4@=^_r%UsTbP`9k!7ZWAq)s zmF(75o4av_ygK-#rwj2<9Ppa=5=ARmUn^-Kgw+Yu6S3HWOq(^duic|7p&uBK9+DvA zQHU^Wn<?=YuE9M{pkO>&XU7k{Y2~C@YufcFru3w4mt1{k zS9;?OBRbXN%8EfGX!Sc8f{TWESccnh ziZs%xrEFC)eRM!7(g+$tUA_*i8yQMWK_2gz z0fkaZUg&|)e(krM1$7UKT8&GZ0?ohM&(sMaI(c8#T_&{W+$&yu2geuTNIFiq8QC%L z&Bl9?lF@@BShAXDKMlWlr7|~nJ~v;Mt^lpRxS07G*}U@9F;!)RyT~h%sB52Jy|VAv zJ9M%}N~Chf8ynf%UYjp*?Ir<3c(7)K^|mGNu)R>%cw+qaL@YrVkT!{WHUs#LhbHiqQ`|OjQNBq;Z0zN z@Xi|spr%^9lWX*mloDB%(^6<#rkM#X{JW7ZnjV+`(S%vfz-Q|0St@zfMXzR$lG(pISE&*~_nGM`q^S9=_I zQjWzKW=+c;BFrK>qHy+qTwa^Nx(GmuDJib<&*VV zpIub?OfD+(^ToRG{mM+{y3#~H+VqQ|}qX%-O=eM1>5&&bKnBtsL z57gI8q|5t<$>e#ci+?IqT8@pk$+an*kibkGa zI!vDO;w6;cz9%9-QmiZ~y^a5Oj5B_R^46~hOUz{Z;WmD6GoCAL6h$NPzcn-=sJe}i-rn^$2bBh+a@kE~1}=Jn&Mk(&Vm}b5WHo%MLgkxt zN?63;f{oLqnQ;h~Gc)Smu&Lw}y!`|Gg;IxeWB%Xwc|@b0{MMYg6m~+V*Eep7kTHA=D`@-K#fLgl zN}$J`kNS*^s1Lg($qWz8*376QBFR%XpHP+`>DofkLcbn&Zu$Ms0aadmG`Aa0>7=(n zui6@Bqf=a&c9d2m$nY31u_+}5PHuJTV9GS#oTlJ1g`*`%v2#XKF!eu-M`!McQnIDt zt5)yiE%j28Av@oF5^4XzZ%iF=Vd<3WCdTa&mzd6eKZDk8t=VX5-E>NLKZ#?yBZNI< z^6ii=ko#1Bro9LFk7VwdP4}koxFtt;3-d0*J@8nvVM(xViq(?JW7CPvb%fDRqt1H1 z!&#IxK*(N9IGlgMJ^iyy>lzDYm5LZ|{IQrwl=Nx4t?y=BThN%y(f;zPKT4*Z{qpRJ z_@wNT5V%1F7{hnNG!euULQ!KBtVK5;WLrg*{}9CPEKH6w{)wV=RdRRffDJ;PN!8lf z`7_(yp`ifGynWNVkyy#IBhO!}{zNAwes{S}(o$N`8JdjO{mQ6N$Y@8pZ}e9dhsmA? zMS0==!$Hu2joRhuHVIBisGvSxXk2rrgmV>6ZxMt%cEltUVG zlxZ^6{siQV35Ghq!Y3?ha8pV2 z@|alkZ<4*j5ThEkqYd|j2HY~NG#LgCmMxJ@RgpW3Cg0c2WLWA-MeQ6gVL$hHXx04hD!GKk+|_J(v-2o|&jD~ww#9r1s!mNAhxJwaNid$H z7l!wJf>44HTeB;@!es8zO=?z*9*edFQM;Xc5}MU~La~YMFf04}zR)w2KOQnQ`}bhl z;Z4UQU|S%rybw!pP5T&_>0W3GG+FlT7wm*>qJ5MWq4aq$2Xd$&o61t7zfop`-a_u zGa^e|L5s)c>SCBT*c)-tKdzDvb1sTxy!T7m!n6`vE*1$7rSxc z%3BRqM@leu`{veMK-n&l{{Dyy$GR(?kP*cC%`VRMHi-+|h4G8yScSz@r_Z{JmvJ-r zQ?R=6$(r-#{9Cc{X#J^#i>N*nGBz$Lx8MGZ>QUzn4pQMSHnZm1Qt2#}u^01;J>l{A zj-{->;{A`VI@41_ZQ%M}mHiL?mk!Hu2!1Iy$Wz4RAh%r-E=n{w{)G|vE<)Z zCNWVezQdmzV#xHQid_!+7thWK46d$ZX8!jYWTf2}m52h_8Y; zo>Af@kOldBh|^tv&vhGL=h~BSr+NN{Gw?L)kBfsr+>DR1-j%^D9gFol^N$SG@h6so6O;du zAmT9KaYErHl|aS>zKoFlg1omU$~rD3t8r>#ytcE;Vl-F+7T(qS$&nh(hAF@C` zz(oiIe%qUO^*F|N8Ni=2mC5NySYvi znR>lHmUPsBN}tV{->QXN{r?Ik@Y*++hmb50%oZyKEP_3k%po?4cw~c)E5a0OavjO2BDzFQaxP9L zOeQ(e##hM~3+FX58U|FiB!)a>YUA-26iK2e1PUy3Y%Xqe|gOm zZ$)lq`GDs`RbeZxKu@33&=kV`vxg=)T|8XXf1CXGZ@9D|!B>)%^#Oz5%d!4Jj2D~v zRg0|C;Jp!5hrcTu;yBP>Ft!oTALl|flr7Hn#4dFMSl=G&c=pnLbTYd!|NOX%J07mi z6R&k5b~m@evQ<^6D$Q7!J=1U_XuSgoGA|ZtYV9wt9ayz2E%V&o z8Q_LuFd1@#Skp|deY=UZ9(S7^r*ie6nrd9Tmt)0!LK0k6OrvFE6!2+(?@QNS_c@rA zMAA4K&Pi--w{R77F%t^g7j6AAl=S-aC9rE#jdClJyDJZeG1Gx(GC+s)tFABdxs+d$>sm}4A zX=a{@jSl(JC|P!HwVhv*9JI8ZGpATdzn?O`IFfw8d?X;)UBwMCQ?l;7 z<0IFPdxN~%{JwrF0TExiS`zWsr>-+y8|BE0{KxLh3(EP9W|5dtbVH=dM!(@WZ!PLCdKjv1^v`oz^sv)PY1y(X!84p1`5~zY)vJE?;m5S#wzI|aojm#NUB1h6RYq(wp7h%V z!ZgnFg+KM&!$bm&3D{qG1?Bs%8XzR)4!e4faS)$A-;&`w=*k^FKqWM|*W7*tTT?#k z4{u`Y=S+#%d6880z4|Lbp#0>Bv!jeiTC2!M*jGLL)V-%eE|x=nO7y3`a)ulZt5q$w zc__&QQ2!2%xTioKn^3H6We}H9qw*InEceh8(^Sa?R&i;(aqP7ebh!x&r}uXq&8sz% zt8-lypJ_HJhO6Dh_E3g;#)@g@vmujHohsscv^nFsZNIBzW^R!2OjKgq`DnQBQ1HqR z5qBAo6&ba!+MQZ7+DaE>n7#FWBlpc*Eqb9uh*M~+#g@0ar9CTa_y}g*uB9b2wc);V z)6M|pHSg~7CBXanT&Kp(?HND9b+drIej~=viDUw&KH>r&zGOhe;h# z<{N!Sb|svSJr}9V2jY_4F)7JqMWw9st@=M2%je$CFc{nV`0%g#(DU3un>Xq}3C2}= z&S4fau-{XDdM3yb=x7Q;X=b01bmX%@7DJX^xt`%YM&Y?)rCsM)1K6gV@0n(_2ypl79nWn?1@5z7pO*Y9iTS>iXx{}`Sr;_=1E2SzbTYi{#C1|Zp zu+Ww^3wAkoXQ<73b;@>N=~Q^Z3O={*K7Bv8|5`UNMl^+hNUHd;qzL6rqy6PZsl5sX zk73G`{bX1ATc?7c$3YiK8^J)Hy;&v|*%tx+k*U-ci=}D&jP=@+#5R{u93Rdh7AaO@ zyr7u%Sn0asw`VOA%(kYFDvejp4zzucwGb;clBA_#ehO{lu0lvcH4ayXi<;B4XiKM2 z(r?LC+V+O8$bTqAymx)gp1k*T*xqP*ziXZMakJUeV!1*4OfDi?{wAR{T9fUGQ&leb zeUWP6MIn#2=5bhMa)`~*&duLoI+>cyezjoLIe{{xOse5`cu?kOs3JN24d6~!sQ7Sn zbGg-epN;p%>*7f`&dn3K&f-OmU^n{H7?CR^j@L5bX+4I%a9YL_s0>u*0 z4>K47_?}r%Sv9>T?LZM*SEJ;Ytaj1-U{hl%@u29> z2@CK$@RDd&cM&?`?%NNv8PE6=dcpFu4&FAV=(?|$-Kwz z=^YM=3ICjpl=DrMXAt=yEFB4?CMXlIlb{js|OCGW>151>mZFF zdzjrLGr|d5rQxRcv*03xQ%rFSqEo+S0Osc2(zwp6LAO|#1}6?31KP#7`TBRwJmN5s zXWi0Tn=zbeXoqJ5B45Mro6Abws5J-udUV(yD(xoI*zS=u?WV~H#3Vx2g9iao1quxv z1~x&Z=n?dTQM$C4v(j7s1c%deh=or5MEPw29Ap3wsMp4_YUl^s2bE?#`wz+(7nv+?oSum`{I8OO2O(Gdk zT0Swa0!>QFtRLVL%w{0}W-O{o=XNeqK<&sdt*_%WCA>4Feffon7SC|+jq6Rqe3CCm z%)%$Bq1M5^uFTbum|;R9M(lasb)jzA27+4qENqv#OD_7hUsjRAiU^4NQ@U_@@ZRWN-WuCI{Pnb(>;!%#R(>7hD3 z_BMk#JPcty_xbWTUpfY_0yy0JTX=I88L0BoCfEnd^%Hkchx3_@Vd{tUTa1QBk7=_S ztL;Cf5a!1|KJlA)!?^6e7|+jb ztCTb1Ed$CPm^QBnsMM1=qw`CF%vFNsFxgi&e{E=R#?@Z{L?vhqI$o*LAJxdh1~L0UhH}M_N_qb-UtC zS1^mFfj0XT=JXP-==^>Rn@zHwQmw`jcf$9Qe9PLMhG?9zdEbRyT zWITXjw~~$JTm64Ty=7d}-~a#r1_YEwq+34%5(AM=DKSW8bSpJFMt8#mEII{+vBBtW z5Q&k(7;L0SHxtIl-~3+q|F6ETopU?qyv}(%ANR+z&gB%l<9O53QgK!GE;G9&F*g1i z8y}Ni;m%Vz_r4J^0(bqAjHkv={DF4xZ@{m)@DYy#f?J3U=BP0H^p#_d>|>s_fDcP1 zd*EH$xf4hA%O|I2J-e=Zt1s#==4w})SSMwkO8HPtD>k~&n<(c;lNMf23m*EaUI?{r zc3mA@YhV6Nk)34w4!G(syc(h)7NIy#n{A7B94Y^-Yi@!$doi|y1<$jWV#s_r@9tKN z^hQsUlRx?qkQ8(^)paBWrlmE1&HN?*fORCWDw{7DK4j2aFgV}I%2Zb|_J}jKG4Qtw z9R`s-vFjolb;oFT!!~+WN65W6mvtn#%|GvCT~BU2+u>9vH!5pxeJn@9Hu<5c`4a`l z{Osl9hfnrhFUT_z)-W~&Sz$Be^@=uFM=O=dzn5IVot?%ru1>e`T$v8~2FtbySNDAl zD}$yeI*E&^pLe$t8%M97)U8xxn_@3b)(gh}K<2d`w$Z)e92E8qHEjFPM%WFQ4&wTN zopI0ILEZ+Ja49D4b)c4~Mg|C_y<2*`^lew^b97Z#>%8S+Qr*l#mTunp?uG4f1Fc?+ zDbCq!2Kb48mv~^Ka>wkp2L11&@oQSY$5nFaE0pa%8f8!ptRjq|PK~cvClfQ`h(}@b z_7?hS+d2L9)%ePfB#%aG+ybc``#nDlguR&g1Y>mhqo``QF#R8INUv{?)?wa!1dR^hgcL4W<-|O?uvZGBWl{myXE`m^X zxF2%x_0j)X*LK^8FSgZrlC5KLDu2F6<%+?trD*f!AOrVUQMo^2Qv=K*IyF6!ceDVF zL6R)|%NFM{v&LSweHcKjsr4(?9tG5Nf`iDAiN7N*EDzLK0db#dtkx@YTx$8-o0@uS zI{!|oSincTFlOTGTUflfhxVPaO5*1flXza^-M8&*x_gW#cZk1$ntyLAEaCV5W9*}q zrE3c(1q-c?s~K#Hf0pyRp|KZ4vE`l`B`F9?@#j0hZwAaJmhWu+SWP20-I#Lbnb(Z* zL)(hazZ^_Tb0P=BO99J)8RpV^{d8?^Z&**iwY=i5yr(E(>WDpPlYjPnVaV3pXH;qS zAk8`v=Z0m=XRI$U0ryaJr zrxGiEyU7)5%^?AlRbAI%e(COl7fif2LL6U+l5l|JH~!+1Izi4xpQ?lvKaQ5@S>uVk zJ&>IH9Fvgmu_qRhVoW}zp57>{hC_(8&E0Cdnk}N!{!DQ9+G_d{p*G-QyguB~v}*K6 zy5Hkfqr3lqhBI+@w8mQW#uWM*>D%j~`{LPTzJwMdSLz*9->lc2)OYleGR~{8UyT(2 z>Xr&O+NuveIqF(z(v`J^>Nw!5)@#;J-)ehT?p$oWZ9U#?0y?dab9Y&)aXIa3DKMnm zBik;R2>3LkV9ODG5OzZui0{Bp0#)&KX)+col_%sOjAWWp_d1m4o+qcid;Q~M3wEz; zQ;xTmZ}G**x0H#I6WgW#aV|E^C=FTfzF22e2k{>3*1S8lHyH6R+pFC&|Ion6snF6b zGhE*osJ7Yd_T0ViCoz8K39AuZAB8`W3Z{L+%2m+&sr)&1OXU*3A)xSMk#<7e*Ve=> zq0CCP)aT-Hh_|BR5A5#3JAL|w1^XN2*i`Q+DJ`6_YO^><5v`d&Gb+O%bVwM#+0Rx~ zlpO&A9Xh*CcI9u{d~R~MJHmI;W}?`*TU#dF zlAFhh)K1^ky%PK-^X(eL>V$fEA&sAGIUTb?*;Ux7;&+=a!1#DtH{DrqW7KIjYqk}` zHm3hEr%cXPFORM;81^WkPjc$|Z{_De@=@!*c|ak6Z}#RX+4fj?M9%mQBoXS$oxeY@ zKQQ8SzN?m_w1sWwHb!rZ^Dt$dyu<6KgfIP%AX?<8I4$*(YC0&wYwHR1iT+ZPIQP0xu4!F4RTK{^5>{9&!=sDKi0Bd33o=zVuY(J3LK~wgS zDcV{)c3slGQELwq-4V6^PM|u1hWjt^NHqDoZx_0u-kEh#+L^$^i0wXb!xlMSbT~II zA&=!lK7RGo)fBs1vWIQN94_fr9K=}pAxiYc{Ueye7BlzIfB-D6F;)huh#gP7SFQ{S zpc?HzdhMleS}ix~{=7X^v`mI; zW^La!-7BEx#k7|N+7JSON>%-bu#G>5C%XqNGiy>q-WOZBx|wW_67&sd*T?y$f8ShQ z|IBrq50Wly;ZphI1$P+>+e{^RYXF>#aJOTt#0%*&YUtL{=3$znm34%m*Qnac%93r* z{+)E=OmWZZ1>xd?VK++I&2lqARhqH@2N4qQW~@K`{~V{t0`|>9?L&=(t$hUt98=YH z&&w$%^4&T3jiFCsla(xhq3=)o@se90Y>`YN3$ES0F|90tLC*s4yr5Xkk)XL1Spykq z_b-0iqIF+3pU4khNVbjb;_27ZI2#uhlsMTC74$3h;BA53@W!px7?kI1sb}J|M55Gm zwfK{AE42fNp-4-jBDG+@XMxGwposD~;oZIX3PV%&O#&ro7-N$q0!n!bl8bJ+R|f z(-1`-rJML^b#a`dt?rNuWJCo8Sk@|*nl9}v7s3y0QH`2%ZroximnGS--o<}Xrch^&^uUe> zpcNIA0jwkVXvId?%inE_cEr7=M*~&V$sa_Ia=Z*) zyYF7?O~cjlr##FL#!4#E(^Rbk+UyHCt6Cw@2h2d+&&Vkg}x zrcyGbjybY#1rpi|2RO0vDrbC@e`;g0ZX}J38zRILzwaCXLE)x7u2RwdrbVCwL%FL& z^}C{YP(dGr*8c3}h;NJNg2>&9o5h2$57CL|@9OBeZ}tR?PwHtOx`n)FyWNz^sroUA zIp<-}>fV{-ri4k{xw;9wv?J7E>9jVj*Wa4JMe)Om0p!Om%~ zZkJKYQ7wcS`Iy#cc1+wauazgmeDdOCF1JV;-I5;!qz^%a5iz!Az`o4U#9O0piEwf; zrimsc?7#i^bhz+xhHmt&DgLrb7z5zOS{g|FLypDoMPq;Ce_wfaZv9*>Gm77%TI1TZ zH7m!I1bbdBso@?rKqw{@vi5zFT3qyub)H`_0>$1QHEoyWz$*YZ8L>Owjg`BbaK7Ts zLBBlgqywVO{P#@?^ndF{n233y|U?qyqOjnMD@qPnoX=bp0rEQC_= z+m)C`Q?<$`7bTe!d8j!jmu|lSg>UKh#`;)AyLc~j`a6-*1#V^bc(I*T@4}0`jz~w{ zr&8*~#UiYIY!B>%BVq5v`gCK}9)GD2c*dZYnU1`_SwZSp#zJa>ng;U0`$Ou(0N!1K z9$ZOOrSg}`TuYW`G)Bx5;mr;b2*=R~qm^c=QdPFb3OWC(pc}H3Jx&E@$8WG+MCn_9 zMq3X|ztVJcXft|T7ZQzE&@~y`iHRhkqPMC> zW9Y%ijZ3MPtwUPyU@Ff}8s8ZU^7kmGy#L=zzabZld$T#Jv}}gQPl6tHHn4lpY6+@Qt?~0M)J7eZGIN*w9UI zOtJZ*bNo~B`i0uS)MbBB#X*4A?!(s?MH_5f_2YtnmMBklfi^D`WnW9hwip>wSQBs~ z%}Sauj_T88Z#9mBQHkV#zVAi89%Cz3+IAL#dY4#Ox_koa@ogZO--;3TzV%XL!YVOWlXW@%Q&PK!osG3 zj?n5exAnImMDL%fM#rfxaqsKhVG75XipWp*V;%Ls+6Daze5y>sJ%{cp+S5wBN6Ybp zo9iOcK&vAdH!Xyb*w3?E0ZM7oH>9A8gM*V(B?o9~#&jt{1P9q@eg`(Mqy`1}gpW@U z{ZadQp~`55d}<*eN=eIXOBAD$G3xQ92jQ6;$Rg)@6vFI5!S8uEVYh4+zoJmV1?c-l zJ`Oo-qj6?>yci6uW^-cb?Unt`sAAI4B^pnrEGt%8k5ehM&K( zx)=;Xkc;ajTiYr92lT2m^SOW~K$Xcu-z4L8AXnwJlEA`U5=2y4%#t={}K zeg9IZjy}YWMlc&7<@y@qmkSe791V~d7@jJbda=sN9~?nBp!UO=SL!D>b-gO?YQmDz z8#V_~w)&duN!{d*_f*MBT;4I@4^*!g^k6IyWUn9z|oI{ zZ<9yB)h%Oz6It{H$O$luE!=k2$Fen)P7QarQ+cO|z#y;t#N6oFj+yiRz{d-i>zOxU z*^C0KP(fALP??7=eU*M7M&^dcRFAqRb}^4%9cl+LIA-oWCsWH(y=bCoPQRU%PL+}F zcKyHG!!akt(bZi!dS9ks#`ky#&ZK5hJY!_h4%Cy6-?fDIFS$E?2?*G?reFk5GQh$F zoEHV2b;mirkrHx|lWN-_GVSsl@Fu-yE*e3Hjy>VH{0A}%j*d=R#94Sb_$=fJ>!_rZ z7QN)?lHjRjDhbkoH6$UU~3eSCcenn(36$7FQJF_XLO7A`_HA&hM7ci!ns}*jisdPnQ zajew6)va8-+fh#ryUDuCFBoDcYZVl3Pm!2pTLZ^-mZPIPxFriO_YXSSD2Nel7LVQ+ zKqlG;pn8(HTjF2UT34tk0Gghy*=Z1)n`moF?%}%csgCM8Kc)PD!uOor?TnCikm)WE0^ zF!vP&Dw-Hw(c1zse9~+J(Gs#WYUzR2apP!3azExDK&VBEev9yWOR3`aDrU_|`8hrU z*C(dvelLAgEl_09M3*djt@_HJ=o;L>VSJ9qSyAi3E45ng3xlaltl(~;jR@lx2w3Vp zehnww{DA7mTRZip8^WdsYsY5y2H}Uj@pshz>7PyStUMkZ^Vv%uL@~A;#QcQ!7i#v6 zn7oLO&pv5CBHplB*$=rbS4Ttc?M*wfG1e_e2dmw7d(#0HSaz!yJNhauKYemw;QJDF z7I2TkOG(;iuIlO|NMY8BD!@=x!fST#n(r2AwKbCW6)?6NU5zPD=jJl1l|Czzl%bBmEaJ2~)_jDP^CUdwBG) z!=y)32J&{}6iEv2O+Bo*;DL2j|CITqA@Zf`(bNs1CXmf%5w^r=R8}Zwhb_^ICR7Ly zGLD3pNn7U*ME=}PqSQmgB(~Zo7re?jXm{`v%x2VcG|r_98x0snCU(={^9VVe9;E@a z^{NYp{YCptZ?MhJu;S0^t5eMVaMMLFsNxm(Hh@BBLc;g_Nsgi#1#F8t;njDJ{ST~~ z5Befo$v&m$gMEvu&4KOlJs@@|@!R=pJ4TYcui-7rax@x)iXg79W7b(?0<$GPWLb57jK>(blUq< zSMBfaQ&ly65|&e5_qZ!*~BEvPR(P7afSjBccQ}MTcbg z+SmB#+2C%W&9gcIXM?G)*18}%HAMIxui$QilW$+jO}Tp-(tr#&MRHqB-BP=m#7ma@x@3YWR6Muvjb_cv3}O0+!E{u(dX|5?c}} zeNj%3$=>L7$J+hYufOBb*WI2cZWY3VT3SYbIKzgL2b?v{9U6I0^4?a11p0MCk8@js#-F%{WoNj|KmTk!Z9{j=U8D_g40@`7StIl1n|+>dsz&zarn$|4R%2N zC2A0=bIY;RFRrTVZ(gA$Z3(S&bwDS`nv|N+8&|8;h|_3IJ#mc^=U@5o@nOKWkYEuB}U(RPBFvVf)L#-sTyYsz<;$}<;a$GK&WYWAO> zS&%zqyd_e!tNfAKKSe&mmi#~G4CJsMtr$C;`6{A6q) z<$m`)yDDnAhKf+;u5sbWm=8e6L<=kv5WfM-Zvapp!0XaOY_faK?JW{bF(wr`K!Ek3}pc;>YAbvHxsCxU&-aziG~w`VZ1o++FS?0FDbYf=_AFeLWtAbNH7fKRl9t-()@|j8 z9PUS{iEsgB%fL#oL{$T3g>dvX{Cu&i{AiJq1HV)@Ty_HJ%d$v4=}O1#0vHB2Sadm0 z*$^Suf46RVb~VK)23-0AwVGbH(K6zY9d4&&tg^(lZH+mjnkVyI=}gIX z0LC~r1Mwc!OYtS^C)#&iRV+ne-VLrB&F39OmgIHn9V_;gzDKRS#Ru2b!*e&*v1P+? zQ=QSR(81HyfG7#gLk=T}9gy7qfw^8R70%Gc!OA$I1ja$P2*0#MX0@JmsRR3!;YKX( zhDg~jBy|z)dj}I)uKugqszXPcWhRdKO#9(94=HPT8;~J*wx0lfR&D`-vqgNspWpeO z3g0dvh>R(>=~nYuRDUGcuYJI?m|p>0cOwXFCA@^1#fd^08+*BFO^!kdirYa0k;dj&f3piN^W324lcJpWtSF^R2q#kK&~eVZ&2X(93CUou*yA?9;HW& zSr^oIR{p|l*&^L!f23&}C&4l87ix72&%tcu4^p?m)iu}>k^>0r0DbT~bZ{kF;?o&Hqd}t8+ih^YCQf3{;&;5K-jH!(udW^+@#|MCrQ!bf$Ztq7uJ z%+-lpJkKD@zav^?rk?_&vw==?)jyAxSLqlu2(Fg&%aUIWtl!!j+vN<}|K*gtla{#t zxX9z<9@`P3vu-8VSA;s^Gt9SZ4);H>fNUMce{6`@OrA^3jcCPyR4r^ z+UgB&IpKsfc-YufP~Op95xc#u)Wcjif&kaoo_iZ)f@O*tFF?Ncn!_QZg0$A?1FA_z z6+sPWfcvx*wosrri0Ell1UYr+B%pTvD{7dG%Ac2Adg^JsH~gskaje z)Juwm7R+K$-HXi0T7ggxg3i(R^1KkCyn4-UdIZ z{mIs%9H6oZQif3f*Msx9IL1}{RY(+S<>&g4E0MNhh0lNXuIt*pL8jM^E$8+6O=(5i50`6Qexh|RHWjirKKKHsAtyuE`-G(d^9najkxoZY7AdRfIWV!Xr@aQ!>6P3SQ}U%U&sleu>}VQu<&0L-~8^91nL4M}u4CUzp{D5M$1 zx+7c3Z<->G$6DoWb-)Z~_isOk3kYdeLxi(N!%%h9iG45M0?EJIo7TbtSG2qX+Nl8~?VnZ5Y8nV!>ryp8{aiC!k4jI2$!EwIx1Fn|D+HMC9)}n=Dq884-Bs197XHZufP2mR z$g7RKmGA(DG(iIqu=ilGd9mw@$&`HC7GvgL_eB$IQd`)p*`Ubf;6}Oy#bJj{o1N>| z2$MvDT42r7ha72ndMqAqf~vzWgYWGH3ijV3>>)b>PG*LazzmFgV&oqWcb1Gf*F)At z<3mUU8gwJF_zr{77|zn8THWATccLdr$+=x>Q9VIx&dEQtz$GGCH^Fn-E-L^UI!g2^9i9Y!_GbQl8`;WsKdRW!$)yb0rdH&MsO)^U{iAC!rjc z)c_Nk=}{W|k9%KQ69goSm8gX(bs#)O9Y=)*# zcq$tzTG(quK_?Wq7wWMvSZx9i?nd+xqN&f2ct{WqX%H9HNQF6OS*2Ml@jB;^yUvPv552+)+U(ygU4q{Ct@_E4%${`7x&?&1grj(3YJr*1;o1xMTJf@V%>Y(*K zEi5~CV#czF%`d0fZ~vN0(bAG_C;Anm;asni4A`l|x6J_NGa_*F?wuuScFB^8d7y(} zV(@;0iUFX{mDeG|&i_ov##N;GvzK|8T!m!_@X8TG(ir)Ctk8m+yX2MG8wE?4AyKyFEikHb(iTzia}?t z;{coNRKA>o4V$uFyZHzzxhq@AE&wB-2^2DU8%t_pFF(>t7>YQz`R7<<3w-q;CF)N3 z6Kqs5!8%A(|B#Eq)X;t1J%(SpVKjb9M3JT}iI}bt$Y!8@Y+)1RWaa96D5P#P=NC^? zh(r%EG>6&%`$cmH4pCj5l`EFx31dF9YUy=s88n~!gdG*DC7|hQj$sNcsc8MQXAJQ( z8oOFkbKx1_t~*J=T1jXi14&W;#-XhPCjtrsivy>-&R zEfSXTKdCc(4-d#!6GE-rn@>G?&BPRRDy%JX^~Ex3EC8reAh2fx@p4h=Um2B%O}ZIe zaW1_cZ!1+D%RR_`L=j6I&XCN`O0Nq~;pA*SvHce9*d2q_;Z#N^R5uW{Bg%I|-uQx< zBV#;?fvJ_s47o0^ImlL>$zBhKvaB9PRe74LUpQF>HPV|pjp{%_Ui~;2TcHVk%37jh z)hJ|TI^ioUNL0gfugvTg@)JO~2q#JJ|HL}ADe%TEuhVBPVZ)<1U0e;HK3#XzhIZjj zeO>OAG&93IFL^%XpQ^1|QzW0V{c!CM^0X|SDq*h-ZkZhZpU>4$P+S%dK|$5V-%Bw0 z!@0KSI_lz!sb9kiIcnDYy+(SU<<;`LT94}!E@H+Rdm`OXEGl<>pmayV)xOiC;IWMi z)8Bc~Z`Z})JIE!&U1(}?O(7?`LYuX3%N-Xq=4ao@xof4);W@Wnv0Jj$7JoMd==(mH zeVX%BeGT0SofL{oAmX$!eUS>J`BfDggJGrSs5UDay_4V&w9W0P8h6)x+m(>Ki zBu*v?sI1cs8STwse&{`**BQ;PU-r+4X`gyO}gN0E76C#QB^*ZgYZ#PCflx=nisswNXtmJ9e)*5HE)Vc zx5n8ORyhF`Dq2r^sz;tS3_kZYS=-5NGyph{AX$MVp3+>rFO7TP^b+ax?pCH-ifTrZ zCw{srgXrSRM=_%ybL45=O7x!}Bp{2zhoI37VK_RCR@Ve z%EBIbhs`D9Gm*k~&~dzFY^G);+hc$^|KFI_&>%AMG zS2_G?T}7T2Lt}>Bck1sOitxC>1;gZGu!6=g6Qc z6W6XRr$f6vlL%`7t(k44Hn`AcM41^LORIfwzwUe+PUy*1Je$@w){F}f3o%~NZ_!(u zhGNdGo3BX>k9ax}TybZ3Vr7wDwO-QoNn2>A%J_cEoLoI!X&nc2=^5(J!OavW>&=m8 z-g+fcQ>v~uX0M&VBPv;UF!VG<0xj^?==hlvY4xLINO9c(7pG~7Gu@vTxO6{?1v

UYOO<%M*n<2TFv(MP1BeDl^dNMLr2*;BsZCm0!ne zwv}UbGp)+#mmr(|e3R_6`MlBf7dzEMjAEta#dlb2j8vNT?=>!s*8KbN>oea;fgPV( z4H=k={$3VjxwN1orgK{kHELa*UdZq7etE*}5!w(iCj03AwKQB#e`d5lb2J547R2ye zbJi@oK?EaI$Ju(k=2=H;LzUDWTv|~r6V)*MF=zCUM&U;&v0amthm4sEXdQnNuj)DX zkJ?k~ux)x&|KJHLPt`o0M27v`@D@8q=Y%8RHl{llE6g|Eb>*dB5!fnt0UdkHT!~^D^5uw2=Ir3>kH^XOpL-8E?SkOYz(^3Mdd_< zHQte<{a+Z+T3;_+=_bVZMZ~yTtDCsaOt~^7N0J&3Mx}ImUkK{**kG zT6kk09JMDdG#fFiySkcIu^gh>!l}dbuK#Tqk1Mqf1VXtU0Y8|3=@t^Pp{7~FTGIDm z;V?;rb}E``hv>xm8S13#mJVhmb;X^8qb}tj@lp#zT&6*@oQ=U8M_A10$i~K}ogZI> zl5ewOD(+a~42XG?G^0!36kj)`?^O3uhO0QoAxW7S^I#H@1s%^?o|<%ylfz^JQ9Tz% zCo1wfyp7xKp;asqQa}n!Cm6a~ zf3p-=^J=(_zv>fvQ;9^YC*i-E$FC_c?-+WB^dCyAgm(2_38LmtH?TbDlaoK_maK?9 zg>%0hyS;;mlb^RVHREVTU#j^%KeZpqyfL*3IaS>Lo!M1VAhBeVmR;3UHagB)-QM_1 z5aIIa8F3flR)p!xSIwaXoj%7b1-5d!HCoLRjtO(+iq46Nn`fqK3z0WS6Hj83r6bD; zJ8sSXY7xB?yRWa0tNQi#!tv}*1ScygJipBwad7~d&QfW;>#A9LJ}qgj%lq0Oh}Yd8 za`T>mho{_1V~Y<50d`Ar|LvKz7(Iv{iMak+qriUBo?dbS(^ zvprsG%@?W3NCB3Z$1_av=(Kxsmgb>Bkn?QfLW(%=SujI2(idC>PXd%pU~CzAT_8J{ z@VllRZ>>3qgnne=v+-SyMh--9_+8C`KRGkiX|AVB03quqHsJ9hEyvJ}2?NSL5yd(x zbg6FlaGxH;Qn|yuv3%p4O(%D}4>WcRTYQYg8pl#?6MNxE30)?umO_a0wtZ_y>kXR< z_dVu*#OqlTI#yr}Dx&4AqvAje?gj^08oshH{*}R8cL(MT(YEd^qXNVAZu|_^EUqo+ zmh`h>y=Y4v7c*(gYa?pm8pz5Ydk&beFA%P0?2A*q2zWi}YNiFqFb8Bf)F}O7Cba^H z?7=6Igc;N^(+yxC+X!dj-<=5SJwv<*@529F09}#VTeen=0eC%vjP|_Yk~;Jv+paeR zu+=r^${~NWd(EZ(Jeks^fn;uiWd+`{!i3g4itP4`HsXhjdF-hZApGiCF82w8p>}rI z4`Rp`%pF0ZNn5KMoi&hCS>X>vxd;7}DT$)Ul0Y2MiJJw@hB1o6+zu+1-E82cqxE&? zU4@cT2QL^HCyOMCR1j^=wtB&@FW@wE4t2D^&v%N=$XUmFudK-jq3Sv}dDOJNXHO-$ z{0UR4f8>x7rkSfbuBUwopB%ExrTL4a(QRX#SFZ0EsM#}yU0~6km7)K==Zn;4Xa;0G zvm&ok`yd?uH6{|6faH)oG-&`EbZrdRxPZg@K*W}8Uv=iG_10tQYs4XDy>iw4wqMBg6aUnDGDK0uvmr8WLfo@`r1 z0nGHcu?6KB`T}Rq`F}^WN<74-<71AJr$+ps)JW5PcmqQc>!`dKb#=fmNcLI5BT5S&(B5kzhEzYzNAmuop_14 zom9Qr#{CwKV0t{JDN6Z}bm@n*+Hc`m2koScs!wBDXO;l%x%DRo#=^*mo3&hde<@gF zuc3|SOQo{)#;9dOfGiMe4GV(LMA3iP>4UHT%L*AvtGIp6)B7^r8MhBW4DX zr5~DxsN52jn_#^ZIOm}JCZ49Ivd~M)8`PJ zsvZ&-Kg<5T9(p3r6R-7`nrj^OJOms*Gcqt&+%FQv^2F1~*GSCL!kk%eRc29lGE!th zt}~z#&FN7h4Dky-mxsEcMJ$pD{N8fnD7oIq*)Fun*37yE;b_zAoK+N33o;kGwwQab z?QXRzlcomUvA093+Dp^+_yDYd{w!O<6%psBbRJ8nH7%0e7B^PiF2K@5vK?FYFPdwu zya?wR@?+2aj>2HMY~Y)O9V@pcDjfapwZsY@NW``0K+xNQxp2fA#o|pj=>`Hq5faFM zl3fb>GV1rU`R&RJ1Hu00Tw-6}xBoc?92<)7{cqI9ne2dq|Ng(+GHtJ;?G&R*+YadP zD0~~ur#a&QWs{mC5zFa=_D-`uw5CqnHlC!Y`81Byqh>T-7B=EueZ+n1TO>^|m^n51 zXeXG7sklV7hy`&@{w96|R?$gEy0)FZauHb@=5Qn1T%=%1r}8{q#|0&F|7AT* zDFzy7S~r)RwM1Hd1qGeDDmw=N`XN6qDhT+_4V6N58cN`E4A@%&w3M< z)oiy(M~l_X)r{@Ndrc~l-a{)1I3se@OFO_|eV;*ti&#$nM8md&YKNo|OBek9gh_Gb zJ9%!EZgIsAH0w4b_ARUjEa|9W6Ah7?_kWJd0eok6Xa1!~{&b|qN(YwXU||>;R}_E^ z9`1^Dw?huEFP{`IfNuF3HY{0{&clN9BT{aNhos1S-S5n}d7f>gk&H;me>Hh<2nz;* zq&~H$O?3WZZv^HTmyjxv$^H-w3b$0dr5IGg{m{{QW_$0mDH-_Xn|#-q_Fc?>`_hYc zQ=SNy>Yq&-zQI0G1GJjNSo+BG4{m%-CEhh4QZ*pFb1l3N>WDi zfrN3FWLr-4$I_M!B0X}TIX`|uaC}ZlWW}z*iDPe7=_eD}V41}UC6Su1L z%X)j)%I=EhJsU)0@B#>*S$&OSP(G(|l$WVHSN!#L$B7GJF*m>oHSM!-3#;vJaho*s zYiDJ*J9Rj_kYSLG)WKKCF!(5Zx)IL&k&ukZ=>a=F|Lr>m>)d8tS{^>XV<;S z7$=_+e~;hVx*-k~f_+Mi_b;jqDWD%Bm{71a{bsyir?7DXs_g@FB`BOt=(Qyt_e^?oUM^6^D9jl>s1(n zYF_tq)lWpP9k)!!r{*!S{__#TDyf;COYDHV@5%3!fMeT`!>$c`m5plvnzz?(`~1w{ ziic((VX^@~Bz$n8|41n?LbAy%6d0I#cwfWo@wdEAz8|W1UYmcuq_=f;$ujw1hW9gYA9&+O z2NVLKn@bn}pzsZVjV}82rum2MgoiQCBfzy@pa>;!*?}#YPe=TEFXQE10HxU|M61W+ z{*1N+PYsPx^KRb~vCt7wd7*FY@z$foXng1?UYkgQbH{#dX0W_s_+ zsXvR@x-E!z$fW}G)oZB{>B;0{==at7b>v{CJ_da?1pxWTyq=M8FN!_7j z2_|}tbzPvr&nq{2ea7i`Lh9(4v>2=+&dT+SqGJr(bs`!Om-MOP`VC{~LZKwD|20zEGAfO8&Y}cD^TXDB@8e zn%~U*pC=Hb`${$kt3ki`?_9HVnvfLVZ&s<`J2SS^s;3o(GjIXk19DalrwVqsI=2!e zv;6IjFQvBT{4VIu{(7ub=I!&zRmJTzY#{(_LN(S#do5o1L)#t%o?B||Ij+JbTnI=R zQb1|`kXVY5T}1bglS2vK zax5sUZ2o0{0PY#B9_HVt=&8)QJN@%O zWH@Fe8NCLhT617*?otqe|=0iKW3iVTemQ-yTE_>D*vO_ z`|Giwp5TfC1Cq9Z>XeU*Rkh#88u+q~sGu_@&z1fm#w%%0J<6ah8%^67B|c;=(p?zQ zMjG$=!cJy}j-*cP`T}wCZ62vMG9w%N!f-&_s8xM#B*jW6eRYx?krkZphO`rV!~cvt z=erwb)h3`EY3?ip3xJOu{5Y6>7zwOxZDJv6=a%f>#(F1&KcoYHnEQ8bs1Ndq(vupE zm_&-$eB6s33)W6i>#l$#TjBAR%((ycd6}UqtWLmkHXN;s@^1;jR&MzX4#^lL4ZxDE zS-13f6kj5uci;R&<$q@Gdm84Ko$bK%|^QN04ZMC!x5BGEunfk36}vU3Sz?LK!(ovbPUQjTrW(kwu-JQ4J)py zx}-{U#_S)p^4Va(&*Jly)c$J2$hrxgT2az(!>8qK1;G;(x%WyzzK@GtwR{ z`XK&?(D@kx3L?H}`|$2$;t!h95wCF?<+J*p*l|ncg{?K@hDK4jCHi{DGV)2usg zk^Prt?*`H#GjCevd$(@)2JF<_;U$! zvB2@-C8Dk7Hg;rSIJtL#$Gp6;3DY_dz31v-A)Zls6L5ZwmB&W>KdQbmuI;Yb_Q5Go zq_|t56nAOS0;PgO(BLk?-QA@v#T`oVpuwfM1T7LsaCdjTdG5XEyytw$m;Zob(dJXV0;4y#&t4pjh0A`L)UeDx8H+>Vzpr__b~tcwJGjZXNVLXX z;eO>6ge2(3E4j||JabQKEjrH^lj1FcQjnB z8mFM;$b(nu{Q$%!b(e*$j> z@+=je4o1EbcPg_^e5dULa}UCRDsp>jBCareow4KuslLnB8DizOib;ze&coVgi(+_v z*Bm*N$-`R8Z}ANe)?ZwFNq95ahDWsXDVyb865|qt7q=KNA2$X*utWBMZ4lLz&->Oo z1(Q(!;&N*#EUI6F_cdJ$M5dV&ugGWBBc|m9&GAY+P0)mixxpAS&2`J=mUJHR6RFvI-o{y}@PTg`QJAqPm)vLB4=rVYI<4$Glv)?P~5om2j5OumUr)>tZM&(S1`6HS?3Amuzrua4qN>?*1W zhKs}k*?Jg#Dz~g73ql0kE>%L8a;T04?a=G*s63M2$9CyR?)rU#HV4W(j zj%#4yL_gv5laU<8&jV z>Ggu~PD2k7T;t6NZ?;p^nnB7>OH$86=ty5XGC!wUqwM{hF0m?Zk5mF(ae7PJGJo|O zM4g6(cjp5E|DYK+M(TSGDlhhuF-05rii|YI?PoD;zT0JFJB@fJVwA}rj&sIivAvlCRF-WL9HfwOKk?p zQ!&ATC213Xg7-2x2O^;1(;~ z;L7#EhtF~5My<_qwf09>Gxl1jN$LD+vU8llyvKp#qmhhz?B7*azN+ouvRraSlC%#RJgbTX7xOGVzdsP2kmPqTKz3`nc?|zLm z6oHgQtJW#HVnJFx+@Zpm;x+kq|4zn7mh&I=wPXNPIC`-e8$nnFQNP0XdJH3{J=({( zOICFDJ!aaTT{5A64(Ry>9WO)g3lpjea(qkx}2cG$vw|5Ve8l_ zvdGf-8Gz!+N7re;+GE`*-Yb4T(XNMS>W2-px@n`JTj@TvMch6iJbQjCZPd@w@dGh! zyDmfA%dV*{58E#p{Q4AOZ?v>m$~zvfB(6VgJd_#hpFzh!#Jkbg$4D;Y0-_p_TB9b> zWy}jcpBdrO@{I*Kf#epO4gr14{iuVqe4pY_6n#L8iYv>&DL<0{V#@oRSr7ZDzs4jp zWmf|OY{$F^hP|L#3Axic0ybTHWL0wyIipB8&28zFJbP#r7|k{K5O1}#RvcigUi`z; zIDm>*8g0!>p$c0V@|<Sjl`1@3F`(| zd*gA1xM<4!@&I&}2`^d{Ir0YlOdgg5R&-k#;8ykgY0MH4Qv~4l$rr~iJ=|=%RBjnX z?44bfa3wiCV0VM)V`Rqyn`zuc16V>WD7jmo_muSVgz5iuUjoJw^?3p)+X9Psc=@p7 z`f53Hlc36y#al4h+l)KkKHkd$*iE&r`1Q#nlJ1OjOx8buv8)KXWzNum62He3^)Cw> zv+Evfj>5(SUPGPmj7FL4#8VSG&NeSfw74Et+Hf`C~QhNs%!qKpX*CgV1X`P{E7eN#H+JM$V5Gk zdXG(C$hB-(Ye0Id-fkYV^mN7!+j8z8Z@ZzzVG100PG=(D0G#c+7^_-ucJk|T2?CNi zz#qHo$8|_sm7}wfp-_%JM7pk~osv^w13|{i`no~W;{!yvf z0Z+!o@p!TqrFCuJV7K)>VDXe;ll8m$&|_!N&SqZz`*2$tjj!iT;C8n_)IN)xamuKV zHV;2dVWksQP*S7izN9R!+vMuW47WcSke;BSeRtbguaF^X(u7cD7^-S2A7OvE`4pbK*yQPu6 zm#mzApMY_?v75|W8iMZXPm2nF0_JLQ+9W&cR);%bA%_@H4H)XI&$q1Dm?PkJ*V$VH zgd1&zT{K4tGP`+~(o?$i6^pTn26I@9z^Jhhq(Lv{H^Rvi*2EHY#-wm70$CAy=T+P` zN2tE(yVY;CCBgFyXgligC86_Xc+tIMt83@kC{4PSdI-YWmu-c?JIjXPjY!n*WSZY? zV)Dyvy%#$xA$8o@PH@{*U7cCe71NO6k7cu&4s}E2Vo)IGfe*8ZuzC<&Hmpi!lh6Dg_KcvQ3j4}OL}Yrw-+6*U z!*n1zN*{p6l1!$QA-L~OJz z(>B|Hg%Z?79JOH>T8N;L;!JzA7 z`Jna`X-ddDhK1jlZ?3%Y7UG$`okC))NSJ-ysoH;q9El|xA(|#XP|-^C_zBU zrOi6E*Z{19+9`S15Qa^z;bAs=Si}>kKnoc@m0q}un^9D^g0H#XXZCn>d_jZ6Jt6^t z0U$~x)mwjxn6lmej-(Mu+azRExI~hVn;q(l`~|Y?Oa9W}pLUnApR6BnK&kK`0BFHJ z)Nj)C79mIVt1`k`gAg8?!gy%Vdz`Z$laqV&u%IH? z$0#H2mb=eWkX!e2fXB~hUhMI{i=REJF5aX)&y9|YT(BB?7e3lykloSsop1>ucdiTm zqo5|~*rn~6%i!&1%S?S_1sg$1^qdxS^G90JJOW85 zZmpQ2N@P*g&^?8f2MXi8dOfw2vXXwnhdaNRM^oE<{M^uVhtv#Wwp0KMkGgW}Ea&XFdE-i65KZ$3!8t%NHodc{FS z=4&plGciEK5BV!{t<4nO0`@0C-0NYoS24@}q~-QM^Zk^Ew#%-`3O!7U!69Gneb;0` zd1Q($*s8G;GZ?Q^Fp(=m*_2B#2bgH@vcOrc~I6ZwxQ{>l`d;V=M-= zYfU*mdmw&wNcnkU(LYbbq|q8Yf*-C5hTRJ`E^Hgy{8G5y4gXB4+GtqIx`1|d zJm=q&NV@TNwR(K8T30ir+&}R|QnJ)!4c}sMr^U)-W(HonVKPx+=o48eUvZ}|cE5u+ zT7F0qa%DM~`Eb9O2D#bXd6GzxI7vAoiit)Ua32{lUH%D7!`(Mh#&UzlwfvCma0WN837*NO8m z9JSQvN>G!jEOxw>l>^^0_>Kw5D6owNwR}Ce{x+|iQcE9-TXx8EByd`L>+X}aju|-e zZEhVSe5lgn_-c-Kxx8Tr@|A&UFM5-9%RQ_z=cCItgkL4e7;v`<1Ve8(i(V;;JdY`ihm^drmtJ#VbPNN5VELh5 zd-SfF_MJm!(g289az3|wYy05WoYw68Nw+v1AT4~N;Z9iI6IcVNvrYpgO^xhZaL&uX zh+|Y&)y}KQ@zulf`-R~z#rUr&$E#mA#DidQpOKYZrv(SosE;2mAW7=Uirp|XgW--X z@~nX%H+w0q0Qz*Jx@`0+#4+=AaYev_F}I;qS*$+{omSM6cdBJl?i1Htv30L%VkC&P zqjO>s{d}Iz-%w$l{|?8ty_WdE8iH-PdGseTQ(jb0aJ?FKRKVs?3Exg8EeoL7w1W_E z#yauy?bPPLSFwX#AxJH3djf+_D02I$mFjKEXnJUsS-15*iaBTvH?QU z)}(sDJ}h$nnv|En?-(}Y3Fn?)((Rh+yj3`?f~Ay|u(al|UXlAMgudKRq_{pC#^-lO zokwa5AS!=MV@`6g{+jaqp0Q~myE~w^VDiJm-d~;I{~2$7Imz$HvCCN&mO!rPHKxKq zblG2mw)fl<__&VyZhVU2Lq+7@C$am)*A;}>;icfahA)`%{ClVp%fHZOPs6; zl8U9A(TilPr4FBMrjEgmvMaR}dWK+V`}&ivv|jc0P|%uWaQx5!WWMp1XDAIwS=S}p zA}gF({fG+4)IMbG+|xP9Mu;sn7oFxOttuvi5@Mq#yTWdFVW#3ft8F`HRjYh43m^BO z)6b8E1Qjcz*PfP2KhV!tVXa;&bm+4MLqs*)qjt!FB^7%~|94tg$frcUqlU#gF1J4- zUJXC?m-L-8sdoNigwqX~dggelHe{Qhd&_EfXG;n|PugA-BOHi5`;SCApXB{N^Q%n0h~}=EdGaXVmLwenjMzI~tto2k`?vw8 zB&h!lP?H`%tWj&HN1pl$l0*zVfFenGWfK4}7{d0H4G_`Je%h`5QGehx?ryIA64s7Y zP}f~sCwUmLJphO&f0Fp(A*UTD;9E_{z(M87b5jb}#o%r7mZxLm zJP|pOdr-T@!qVU9y{mwCzjQ_YsjU*H60T&L@VO6L*1~&3Pod=k_G~6SFDAavZ_(Sc zyh#}}xJV0W8}aysMTLQgOSbcfz#rlkLC!}`ixj>r@$@*V$Qy`X zvJZ!08@p1THRlPZn$%<8J=igXS{H>fG-2mKum$}~*aoV)dG({4CY4;l?z|G4_kE&e zs#7WgK=}%63kzcoS*@t*S|C|TL}8h>HK`l9ZbVu$uA8gWV@n2p=YgwceqCo^ncvuW z>))Ah6zQVw=ezl{ zb2}hlz0{B+($T1RHZ~TCRb#~~O$1ju!Cm*uAj+$EBYu_g$HqthiUls#!dVS_m2tgl z1~dEv1)B-#4`HG2M*Z+%$d3uQ#W1A*3l!p&bY83_j;96Lb76l^(sRlh{HH-nf+K*S z%v-JehkIx0Ok+uSFgCrs5TurND0wQMTtYQGU{e%1*3nH!!(g1RXai<5C!=1`-fKnX=i;hvln|qH)aNGCFwA?n~b}{oH1AT?s zzd*1+7?jH|ms{3#I>G?+@hX=IeSCjiI5F4n+jz!gNmXZAfv?QqVJf9LHrEmXeM7lR zD>%e~v|^oR#w!T!;oxU^iLRv;GkgLB9G^lZQW2^JwupqRC)4zW#b#IJh4vkClcY`H zmdLkc{5YWK$JS~TC+&q2<4!O59{h(PgFLEAqH9ziOTLB0EP$(0t z<;L^pX^9#BOfIruDaX@hM4RCye90~ox`OOQPGWbD-RhRmx@3E-r{u3x%$zCGdD0Q5XoCI1<*YB(Y z@#!G1`S;LMV@YeCVzvEd?_`+{`^I-VRi=ohZS<4xSg#tvgki96&_LK_ybqhyEy43X zeKs0Vd;&q~DA@QdsP!sZV&3lxQFiGBualt-4eoPOama6&vX-EE5@0NOSzNr6 zRuhO{2*Ch<3-JjiM_d)h3jbg4(I~|Izup5hh{>OU&sqo#k%{)+n$?(lUq%GCP#ZB3<&!(LOTP|)!-+Ak(8KL@{)h1WU zsyC)tXOI)9mZpU)=k~8MH}1&PJiK^_SGJMo=7rd%Ye96hkLz2Pr+RE{e}G3b-pU6< zO+*+?A@{)B^7!Pmaof4ozQjGTL5uyR>m!4Bq*mUe4XN<0dYhKLT*(txkGC!*E#_DN zWvqLc-uu1FYJG9Z(^b+1-w)w|9F3t){y(A04_sK?vk+o;@54T7(51499IdJB9`Xh# z(M3GA=)P&5)b*#W15i58?nvAriL%sO{SEYCz)(4deRWg#t7-%o=+t%<$2#FX> zH?W_o`(4bh`o}QLJQEqLJPUD3tUyb)25xaB77(y~x6BOtS34Qt33euD8ZNs0Kb7P! z708q8zX6IT)|UJ3L!A&9rR-`lO|)Z4WUIz%GC=wiJTi+3@ctC%2gI2-9|3^ED7;q2 zD(Vd4;2nztH>QM@H~r%my1IUVtv(}`2L0L*X5(Ssn<-qqaB(wr*BKvUB zxes@p+jN%}&3b$?c+SjuzK=Wtc%5jz;!N`E64t=DDr-1=%h1-;^DmNMEJr8ZAEo>~ zS9qxbnSs4!5TI#C5pb_|Iu`z?p?&8J$p_wNA(Zb*!fcZWi*Qh>SDozexy-Wsv?$;q zWhvR17#|>|MZ@D(tBVvGETmA9miSlfZZy)oc3so}GslNJ590#3?F(2b>1L(tU3T4R zo~XsS)#R$i3x&4%b-v0zj?R4%Qa#09-18EqeUd`jaYFmblv^$v<5v&`Pe@Sn-iT@( zpW3D%2Iy_-dPVrmXMhbbt|xr!^NiYZK|}7Q%eWeW=c6q5y_0TakF07JjT_F6W#dgM zPUw-|{YZegE8^oC&&Lv5%2IesnGzH@kl^5Z+vV$d`n2ejjk&J1T8OtHp9kzv273ZV zBOh968a^Ht1QnNc`tNm-HYyFdGs5y;bv1w+B${7FA0Dv&`&Kr08a5Sfv8#;=!P5f?94ZdqLT@o)4e~BP0ACeJRnFYSPhc*Zm>Hgo`#2Ani0IIQ1m%*a^JZOW z&M;bc^6W{-j3GQ)I;z&RLJmu}Fc*)?JVsZn{$!YN?_C2lccB5Fk=p>M&+El&yS!GSv(_rWrt+$uUgYlU)b_aweYg^W z-fQl4T;x4Lr-m?$J?5F3F^uq(AKj=O^|{+y*a}AyY&s95TK!X3O22>EY*aQOmip)= zi=WyIzAsl<_-;k;c~0!-^O?EiFBfilw@y2O(d&E*Rr+|65`y4nt=otDJLhNagQBNY zjFAl1FdBaC4|zGgu+*<_oEp~N0PVtb0uz}GUbR@97uy>U+sNsCMrDs#E5!X>bd1Zi zT+%*Dg!uU*>`(P1EVLm2wTj6LWX5TOuG(rywc1GH$N}OsInw8|msgLI!QNj(-DOi6 ziL~UZ-Wbfer2IP8pP;RDsL#0IC(z}+aCR+uJ&+~e&Y_#;_;z3u#ekQdT z%;BeUa{tRaksW)|Ww%Ym@ZoUR=2qaU_{2zw?hSG{ym6r2W8**y+uX{9A}#Iq z(=6&UXBtML9^z&mjks)kg+3ccED5;Uz$HimA zAg9g%c$_B55Uj&OKtqQraB-mP$Z8qa(BNULh~fjo^he-J6r%7=k1-3QxIdlY`OQ+j zO{FTtW?2e0{f_c0lC$n3`O&UJkB3eJQJUOClP@-Gmg5ZY;7!JUka*gg^y_Y0YLKBK zZ0UBMyVB}8FaP7xT7q5mX$%3~K_|sv2;Ur1FGJk$>*i0E&kv>hp%=^LZBTgsr^r3>> zEMdDo%ln+M(ZOs75fZ82xm;n7gIxMu0%7AmjqJg(P1<^aAVa1+DEF}fd2d~sZAWYn zHc)jyRZ4t~SqL?f32PQNbFS%k69eo(=8XCVYS90MQ03 z0rG_^6N2ly3QB0l7#S3GIh9iZ%Pf9$QcZ(?ac!5nU{C zxCY|}k?~z>*f~OlTgZbxr9DzhCL}tU*qvz1KK#u{w{bJa{EpN`V`GzGVE{$FIYK9`pX3J3a%N#p2God0&Wc zsu5lO#*&`W3&A3Tj&|iw!MTKs@ zQ)K$M|Kk0@h0O56LqYm;#l~{vaV*QZ0XCx)IXl*f)N?kHWZ?G=zoEuph{L=m&&68& zJ)2^`Jki_xL&t>(#vjca8mcw@~u}ZG;t<&mup`>+_n3UihUT3JE zssbJE!98z#yO4z6{a>pGtG~Ao@9hIhgdfGc-ldkUdWI5y2Y2U@4>#+y;>PJ~k|Bgr7u_D$ z?IG@`_eJ8*xq<8F#UHR4b3+{4RTCVgc0XJWUHERrLhZhjg#wl8?Sn@?3SDbb_%eh=qQ=TBh0dIrtV>plh zYbz2LN$Qs6qI#B_2Z9cO;eBD8d9Im&2P6mmMw0sBQAYU zJ087zk1-g-7O0caHp7>Fj&FAEvaiwkwX9k=x3q$fNH+Xq3?|@i{u5H#-+jJzEx5k? zYP`{9>pCv(a0HqI8X#0tVufseT1WF?(k>)=G&$H06?oU9xHle5ewI;nAT=QwX#C>n z_5}RhwZ~#AkV0r9cd>CXyVR8H|8v^s;tfn@Inz?Y;8|aVc1S>lezDOw_qS)Fhi~8Q zD9kGa-nv;Dmpe-t!nYx>A)(P@~{=$_~K;@D3X8(A?SEr9$PlyR*VDycGP^>?i( zdi$U%QEFQAqLMpC9>*$bR4c-`QxtTkXE)_80^`o~8J_)kK35*qd}l0_<4!nnsY=)< zOS68rAZhzaACVS#(}?qX-#3}7()YCRiZiBplHYx#(tyXYgVca@A*vUYh^GxQ)`L@*k zjj0&#X%!F_RXLOE@zN_-j@ALu_P!JGbZH{v7g(`;cXY)~KUT_As_2!G1nCNTxmNUP z&6j`N_vXj{Unz3A-$Jdvp;^$RWO$WZH%2x0^=+7To00ytoV{lO6Hu9EBfj6!RaN?b zjDE5VQHB)35+;kB92kGJqW_raFF|FbM|y+9Ovt-Kuc>ZI4* zqt-TIU4%!o_l#v+x`Xi)(G9A2!l%x+GN&$NuVsD$)ECV|? zeb0AC(f`x3TgG4g6j5R1r0B9$yPT&1-w!KN1lk%Er`bdaBM)ygeU>d+HOZU39Usw$ zDN53DRtX+oQnTQb*@Ih6C=lXW^b!agpNNuof#4#Z1j)eGN-~J#H#Q7p&-%c~+yg zF8jE8U{7991Pu04e7Y#irjhb~Et2qSOnpt}YnOZCsZ%HEoGkLM*UWvTeT_^cmhR2B z_t}38T%5h7Yy7c3GaalwwA^*u5Bg55OYoKHzYZ-nBi1mAm1^qxq~tW`(q>B-8+H05 zgtI3o{g*maNr#${01JBX5@nXT^faUQPs2hOV$4EnA?KD=^+(U6ioj?&Hj`jX;l5wY z_?!O9s-BO)iefNjm*v1#V!+q;DPhMUd>C(iN8`XX=Qnb|c0)+=d5}6Kw<^cY1a^@; zRle5!WtXDmcpmqtVV4)1d4U;TnJ$!}(LY>w=K3x=txRXuvtnv5G8s1=YBy3OY1&`S z@lFQV4RfOpw;APAU$nD#cpj?%6evlbpmXhPbM@1R*yhUXJez_+3kvV|fwHUq$A!P4 zZ5a!cyqVdb3$qyyyVSSyu^M&&khsOhRtr^$SZv~4C9$`V?$);f9;UsF5M`UOtX+B< ztTp!Wb%N!(y*b02qjN?3q210L;h}JaLGrir$%Jw3tjs4(b25c zob9&5uiwOc&GN0~CFiZ0V@k!^XIj=|L2PRHi=SG<#+;M7sc}<3XliQdDy5DTNFS%S z*jGP^@Nmu>kM)yuA659++B{%9i>zr1Q)6Jtb5V0=6Y z_+%cePR()$7e#%%if*~eG6CKZuZ+uf;?7YNn_-d`UfYMIc7?K?2L)b>LE z)h1_<$MLN@=!!9cry-&6x>W*vS>mjMbMf`Q7iv>qgMKe7U`5mwDtCMk(r{`bqbe}j!9m-ZZDj}(o{pRD{Z!0KBR1?!_ z37wK6h3hmpwx_Gh$+jt`7YjRpDo(^K0$cwnTf!%3Ij={k`}eC!+&4zcGbgm5a4_>5 z+29lWv~RwuruY*U8^$HGjBx)41+lhgR_QTiFP_XwWR|$D_%O#pTVcUf zR2|b}L$P(-rju=?s`rlbap1>ss}C^4nbadAV!WeG^a*~SoyfhhOUzD2}b~@#v9l!a@qX3JBM}=)hnx^0h% zk~Kr4>$gWL|D81nUFK+x7^CHvJijY5_e{yH2+6=))S*QohGeWo?;p6L7bXIaOwC9K zjkW6c{e9_iJ_Q}&PDg3&5LbP-+V2q3B0p`;dlV-hH(fjZi50r9XCta*$*qBxXt=8R^nDt zFbW<5j|$71e+XzRECf8u(Gq<{W_mh2?m_=u`XRH(Xcx|O;V`E|yLm1`7E3MTrF->N zHaA|pPQ;qwo3*ah@O*SF=fBDFgld_mWRnD-PXZKhC6-TH0cM3jG8+y~;2zJ4!fEM> z_YZDqu%xj6Vm9g;R~&tRPa1-{E)-rBsq$jh@e%~F2$2{Qfk)w%X-P6wsqOl?HB*HD zrK+H}IjZ^=wL|{~fGBP|2Ppb70})enuSA=jmhg6PI1Tv1PxfdDs87 z+SS=SGJoIcngAVAm^zCx6fT!P=MFWQl@!8H=Md7EtFSG`wIovK;%OKf(Q5o>@7Fwd zzI`VtY=1HkAjiew5f%{394huYKejdZI|>aX#Q)AN;`lWM2iuvALxziq*;5Hmqyzq1 zMZf#GS_NwZYvr`x5}Ad3GIgjhSR<{wLw7r*O?P=$X5bZ+134iQ0=L_dk!{X=x8BH6|v)hsYrCF!r^K=ab3 z6Oks?pQ8A)BZBbF&Byw2h9lcv(`IdHCTZ#ZMa`~5RFTJA=-Q1?v))Dr(_b4*9>#xRZ1f)FYvwq*_ zncw0O{j1^o7hi5T7^M*pU%s;)GV(Qs6u&UV>ypYM7?T*sn#JkWg)-5*`96s{onEX^ zi7fM&kPnQtzMf^T6o6|H){rkp=oy-Eb)K#|{5)>=RCt2NGv98$N!YlF7=|>*htTAb z+%wVp)gtu@E7mUhUZ*Hz$xp=k8SmVI1((AoF?MQcrA6szp?%&-0br%NObqnxXB zoR8@z(Ro5Ggi7S&s?s*~;Urt%NY-O)r^Z~|Sb7H~{L4{3m;TLLDo|l$BD1lRT(KZ8 zb#C*ByFZaywgc;Gc$V%OeRXhapjK#HU%>f23LH^x0i4lrW%_VjeQ!YYP{9_p#utG2(z7iWbraI<1J7KovY^vn)`^@8WGNcmK9%$YeS4FD>Ag4jp|X}* zp}CIu*7((Lp8lG7k;iMMt0p(s;bo4@)EoHbWtXHsf_Y{;@cMTA-)GtX%1dI__}AZh z)0vjJ^&9}rlL8X!gzalAWOs3fqJ&_9*vDB8wdo#y3t1!|f6R)BH|rSq7$JMF*)2EL z4LQ&aeiAi2jk6+7(3s`e%Lh>8Iwq*sYT0rr%2t|%SaL9OcgqCf2$e&|6lEGNpD4^k z-H6baP@gYC@d25lpy@vjcY5;)&0_-nVx|nQid1Wj^~`16s!(bq3l~Eg*uTl+tv3UV zYS#|^4HylJbjgWy%J-B%&0t(9#YtHKp6&0{c)XdtQ{lii`$&t<@&ObApApH@WsVH1 z=;c>&o>i~n9N7&DZHR|U`0kRX2?eT4m=jBgEXDPBB`ZMWsm)@|f3d|wqn z-a4AmItrR-Au^^}FH*c9*DGjOMq0R!D3dTbvpNB-%mTh=HPeor&}qd5RHo|jfvspW zm?SD6HOBhezjTB;(yCA|->ZopzLbKDd3IlRb8vRKijFjMdj7lX*X> zt^b_@UFv;>Bc#SqzDDRp&n2n+2NEdY`c|*y^t9uE$WDS8_CRZ!i6Y;7X(dTd075)) za+lL8eK^sgfnA$*IJ}aqh;9(}8~L&3c-q^*bm&!7H;LQSgqh)|yDfF)o0nerDZvK2 z9p~^X&tL{YXw%K|U8Hawp@E-cpCQ{KUHr`#IR{sxlKbXk#$GCVgGZ1V#`+2x!>%dt z@6%_#-nrz{m;Y@hY)0*n9_~}dI^hOicarvG0^hn!WRYt@^m)}|*z4$MbN{K?@MPZt zVpc@m|2IW&kg3e)qsJ7c-F=x?dcxM=z4+P>~e1|^58 zc3adhS!x6tO=0{m`#rX|{DCwXL{x$HM)0RoVaN<8UFu%Bm8&(?KLZZCIo-Oo|>a&UwB;1_JmH4xLj`%kt} zKH>RNW^B)=GNy0ZdUa-yalzy@vs((GI+6jm)v4N)$UXfb%I+WhYG0GoKrG}Hg0iVr z9BP)#*GlK=D5q*I2TNC0930Jprz8{tR>!8Z&cR~8R3e0 zNw6{?zmTfb)z?B>NApGX|Hh49{UvRado&c^y%b5^FG~NJfN}P*{D|O)QA+2P|4$lN z--2rRLyNLlN)it|pz%v;>2YUxO5~gM^8XY>AHaB6xiS(0HqBMTsvV@Ih2LtMzxYGT zKf9E%F3eP@M$2xe*a?ja)|#e{s5DYxt#`@9<`C&4d>cx;XD(g$2reTQF{2jPQ~ZGo zZ%_~{7^wZUw!#fTp9}_MiHNr?bRl&Uhjmq>8Y%V)QQ2>)o1>$jH8Ksq8{j_1te}Jm z^Jq^%Hda}}j;Yw+qN3fP{Pbul^vN>#={c#@`pXtb5UVnu<~qc|^j27_k-E=tl9kTg z4}-Bs;?U(>F&#%;F?-XqSzMap-BAVK`}@c3&Xbn^ znDZM)PnVVI_(%clbzV-4MFe+M%AsywBYWEyi7>~ojE(@6*R`gMXmk8-O4YP7G?ydV z!B4Mp6@GFu_Gtd4$mlmfL>OrW>exnH?92~{5hD(~+fCjw0QFK_qi;2dG=B*Sg|Dj0 zM)zRopNd&LcOyxo%N^at<(8JPuG$j3xbzO>7o0%Tgr5;u*Kco0^7~mO9O8xrVs=D3G zVl~)s_+$0=XupI}M0<1DHHDQ?#Qy9i;l?6a+qI(k-~z~u5X4P8c$zlrJhHBoc}Vkq z3Pg1&_komuq7-?VmkP1 zBaFiNml=;nVJp)YwbFXz_r`6PX+U=uQ!7eAshLNdc>#csA`7aXTQE_cPL6O6YA zbo8oZ_+ICTsNwqRVei+&X;c?H-@E^~<#Wceq>tG22_BjKQRfhQD;7KCnte1{p%-5B zB^QGCR7p9spQbhNZiZo22<_c?X>{(h94%;fi`7oJ&cZA8$H80^&bkjCTr8t3_K*z0 z(kFBq14B20=${YfEX|qK)<{iO$!Hv$>0RVPBXZvg~q=5tbyofqN7IQJ#DRVadi zj((M~S+s`<|7*iHRhvy83ZI_}kCj5^l*$iQDSwx%vR+nh`+rtwqZC}%zwX&I0GXI>nsk$A+&09!IVlluj$2xRoj>E^@>w zK(A-Nh|sXDbg?Al^DMR{jqfOsuBUdGRMh*TJyweenQgR_)%?jkzm$m_Rooh-(1$tX zt~~CPj=EQTcX(`d7G9eUkits z)5Cwc+ds06iVI@oCszK^d4%6K$^QkKbVDy|R&r42x1U1|rsEs8r}~Th@$;8>?ym1K za~K6(6KXl35{SUR{VYyPRN~sKw7DDL&aiQYOecN* z`s#0+<&jpqISFv)J|<7K%tJM6w=)GV%l26>ev^H-`Nryl@#msKgB|7Fq`|lcgjPmaUiXv;fu56yCQBQUFNqsp$B#x zyI;V`oD3NvPmc3lPm8@K9Y@9W_aCKi;&nV9{XkuBTJzUVwTb6M4qC%4xpxd~)@JUj z8)8qpcwh&a?e9`VmVA!1J6Qj__+#7{c45KTOPBve=56-_nox;f_l0cpBwH9~Jp;YG z_IRSx-@lG9<1Cq)nvr%*Cx-=r9sd_Q8Jg9P8r{Z9VHK>C!B3aEDBD@kE1#q7(0FWk zV=s9KYV@umSK;C`p=S0^uk;zkn67PJ@>Yf$br<7X6800@)TM}^JJ06?zW0nvkGCNZ z{9)!E1oo$r*w#h#5ZN5)Gt*;TWBC!F+sku`Y^k$4-@)E0|L17|=nk%smJo=YE^uXq z&}ftD@ans1RGfQU_E?k^)L%Y`eoh zZr9d&?*BA$t>H{4U^piVCr3_2OgQAy33F#Hb`;8GE-j;G92!|JYZSRl<(6y8+~v5I zW^G|H6Gu!Txy_c_>@Z|QG_r}~U(Ubt{rY}>&-*;z_q^ZxKHvA;_kta4D*^-csmE~` z$(`@ycpMzmn8;j}Gl-@o{1t0oQhxodJ%%uXr!a!F$hK}6+EFw|dZRFKVYAUS+BQZZ zN2?pdyY?VS<^(S!`J{5RBJ)%KZ2AxoRe0fu41O0{=w(Mok4fA1 zJNQBrq^=e?OvI?e1`kiW5Bi(>Jfig`gq#9tAk&VDelb^vHr8`ugzQygOmvd@umt;w zJ2|$ru@AOzY=^dOJ{+rg=5Cy)>YB?;!w?X%wwH5RX*mOXRc~hpOchMM?>Hcx-LYsd zkwL7T6Y6!PW-d;rAm$`Zpyl7GTkRQ9J@H0JPSv`TVu$O{l?uAO0ggHyGu2FJ8Xt>f zBs`v>hdl;Id>bDkug5-{JX-@s0N8UYQV&+@O;d}UJgJd_Szt8mhZHFO%=zho65*1x zSH9|M2yURog;YjWR##xLEavBD&t0x0P@jgNn-0e9KGA-K43O0q4D&H)UaE;jV*3Z* zY;(!*w>DNGO{s(9fr48WWdV2MI#AbZ@S{k;T*rxltdn{VBVV1p2-ancebs@U3`+KM z_V=+V;M+hxYCArwkAkE6l|JRX!s9wc*I#$jyRus1?G>FG6l1=BRgT_v*i)y7ma}Ou zbGZBvA^8{JrgxZ@nJJTm-G^+2PgR@|XQ*z=Z+wK?hF7|%U=_uRk4fwHGT)26cfCmR z7z9I;{v!zeB-Xz@gq;i~4= z^sDp13%OVk{s6EKlZ4+d051B^wFB-_97Q5AknQ$)hqu#IY-DNs|T==z-ny1 z)QE>ch0f11Qf}`^XJ4vq9QRss@lmk;DCEJ!;1G~LIM46}xp;xk;jkbh-72!onkwRs zN{24zCK46UN1yyGKU)bQbhr>zglG+-89tf#b<+)9)0MQ0$) zqn2v%eh^r0voG?ryVfIiQ$AM*BwIBQw%utU;ZH|`&|k2j@DQeZf}#LGyQT+V z?QK89@ltT)acw=OU>_?`wz9`@o7I&=F+FAXV}52GeDD*$nnmA;y%~)~1b^n2|1+UQ zyO(=J-gkS)GtoN>tLU56FyTxX6V8rI1}ZQ@37*P@e*U!-%}oLQFlQMKY1ikj9sj=7 zk(lWpFmA|E*+|;dlQxZS=Z7A_goZWc3%ThrV4NVr0jla~;)t~r`4>L2YR|)(h z)wdV6C91o|N4}VifIeU}WdtpUB4drZH-i;F1be)Rvau}q!LI**ri9-9iD)@89#MQh 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 015/102] 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 016/102] 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 017/102] 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 018/102] 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 019/102] 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 020/102] 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 021/102] 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 022/102] 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 023/102] 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 024/102] 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 025/102] 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 026/102] [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 027/102] 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 028/102] 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 029/102] 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 030/102] 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 031/102] 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 032/102] 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 033/102] 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 034/102] 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 035/102] 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 036/102] 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 037/102] 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 0f1def58226038d8047fac2eb3e9fa530700df0a Mon Sep 17 00:00:00 2001 From: nymkappa <9780671+nymkappa@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:53:40 +0200 Subject: [PATCH 038/102] [accelerator] make bid boost graph bar min height taller --- .../acceleration-fees-graph.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index d78b663a4..68a2bdd52 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest type: 'bar', barWidth: '90%', large: true, - barMinHeight: 1, + barMinHeight: 3, }, ], dataZoom: (this.widget || data.length === 0 )? undefined : [{ From eab008c7075f4a806505c4dd0c0a8038cf58bfc0 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 26 Aug 2024 17:47:56 +0200 Subject: [PATCH 039/102] 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 040/102] 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 041/102] 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 042/102] 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 043/102] 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 044/102] [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 045/102] 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 046/102] 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 047/102] 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 048/102] 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 049/102] 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 050/102] 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 051/102] [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 052/102] 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 053/102] 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 054/102] 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 055/102] 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 056/102] 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 057/102] 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 058/102] 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. + +
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html index 07bcdc2f1..0f436f9ac 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html @@ -21,14 +21,14 @@ Fee - {{ accelerationInfo.fee | number }} sat + {{ accelerationInfo.fee | number }} sats Out-of-band fees @if (accelerationInfo.status === 'accelerated') { - {{ accelerationInfo.feeDelta | number }} sat + {{ accelerationInfo.feeDelta | number }} sats } @else { - {{ accelerationInfo.bidBoost | number }} sat + {{ accelerationInfo.bidBoost | number }} sats } diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 8bdd4f14d..ffd8e9c3d 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -33,7 +33,7 @@ - {{ (acceleration.feeDelta) | number }} sat + {{ (acceleration.feeDelta) | number }} sats @@ -41,7 +41,7 @@ - {{ acceleration.boost | number }} sat + {{ acceleration.boost | number }} sats ~ 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 f1f5bb3d4..f8fb3c89d 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 @@ -40,7 +40,7 @@ Fee - {{ fee | number }} sat   + {{ fee | number }} sats   Fee rate diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html index 46cda0488..19c08bad9 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html @@ -19,7 +19,7 @@ Fee - {{ rbfInfo.tx.fee | number }} sat + {{ rbfInfo.tx.fee | number }} sats Virtual size diff --git a/frontend/src/app/components/transaction/transaction-preview.component.html b/frontend/src/app/components/transaction/transaction-preview.component.html index 63a11a8f0..066e0d442 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.html +++ b/frontend/src/app/components/transaction/transaction-preview.component.html @@ -21,7 +21,7 @@ ‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }} - Fee {{ tx.fee | number }} sat + Fee {{ tx.fee | number }} sats
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index b2e55a3b0..c0f5c6103 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -606,9 +606,9 @@ @if (!isLoadingTx) { Fee - {{ tx.fee | number }} sat + {{ tx.fee | number }} sats @if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { - +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sat + +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sats } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 8954e4ecb..9b88678b4 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -321,7 +321,7 @@
 – {{ tx.fee | number }} sat + i18n="shared.sats">sats
Show more inputs to reveal fee data
From 72a5f4a521177c9cc60cb4f7b779708b5e791215 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 23 Sep 2024 00:18:59 +0800 Subject: [PATCH 072/102] amount selector sat -> sats --- .../components/amount-selector/amount-selector.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.html b/frontend/src/app/components/amount-selector/amount-selector.component.html index b509d6fe3..a16a24d4f 100644 --- a/frontend/src/app/components/amount-selector/amount-selector.component.html +++ b/frontend/src/app/components/amount-selector/amount-selector.component.html @@ -1,7 +1,7 @@
From 06e699e52b38b051712a2f4775d37887c808bb3a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Sep 2024 16:49:08 +0000 Subject: [PATCH 073/102] address utxo chart color by age & updates --- .../components/address/address.component.ts | 39 +++++ .../components/block-overview-graph/utils.ts | 13 ++ .../src/app/components/time/time.component.ts | 161 +++++++++++------- .../utxo-graph/utxo-graph.component.ts | 68 +++++++- frontend/src/app/shared/common.utils.ts | 4 +- 5 files changed, 211 insertions(+), 74 deletions(-) diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 5ce82ef8c..aaf480d8e 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -319,6 +319,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.transactions.slice(); this.mempoolStats.removeTx(transaction); this.audioService.playSound('magic'); + this.confirmTransaction(tx); } else { if (this.addTransaction(transaction, false)) { this.audioService.playSound('magic'); @@ -345,10 +346,12 @@ export class AddressComponent implements OnInit, OnDestroy { } // update utxos in-place + let utxosChanged = false; 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); + utxosChanged = true; } } for (const [index, vout] of transaction.vout.entries()) { @@ -359,8 +362,12 @@ export class AddressComponent implements OnInit, OnDestroy { value: vout.value, status: JSON.parse(JSON.stringify(transaction.status)), }); + utxosChanged = true; } } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } return true; } @@ -374,6 +381,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.transactions.slice(); // update utxos in-place + let utxosChanged = false; for (const vin of transaction.vin) { if (vin.prevout?.scriptpubkey_address === this.address.address) { this.utxos.push({ @@ -382,6 +390,7 @@ export class AddressComponent implements OnInit, OnDestroy { value: vin.prevout.value, status: { confirmed: true }, // Assuming the input was confirmed }); + utxosChanged = true; } } for (const [index, vout] of transaction.vout.entries()) { @@ -389,13 +398,43 @@ export class AddressComponent implements OnInit, OnDestroy { const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); if (utxoIndex !== -1) { this.utxos.splice(utxoIndex, 1); + utxosChanged = true; } } } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } return true; } + confirmTransaction(transaction: Transaction): void { + // update utxos in-place + let utxosChanged = false; + for (const vin of transaction.vin) { + if (vin.prevout?.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); + if (utxoIndex !== -1) { + this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + 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[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } + } + loadMore(): void { if (this.isLoadingTransactions || this.fullyLoaded) { return; diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index 625029db0..287c4bf34 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color { }; } +export function colorToHex(color: Color): string { + return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join(''); +} + export function desaturate(color: Color, amount: number): Color { const gray = (color.r + color.g + color.b) / 6; return { @@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color { }; } +export function mix(color1: Color, color2: Color, amount: number): Color { + return { + r: color1.r * (1 - amount) + color2.r * amount, + g: color1.g * (1 - amount) + color2.g * amount, + b: color1.b * (1 - amount) + color2.b * amount, + a: color1.a * (1 - amount) + color2.a * amount, + }; +} + export function setOpacity(color: Color, opacity: number): Color { return { ...color, diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index 3015007b2..f0c73c80b 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -3,6 +3,28 @@ import { StateService } from '../../services/state.service'; import { dates } from '../../shared/i18n/dates'; import { DatePipe } from '@angular/common'; +const datePipe = new DatePipe(navigator.language || 'en-US'); + +const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + second: 1 +}; + +const precisionThresholds = { + year: 100, + month: 18, + week: 12, + day: 31, + hour: 48, + minute: 90, + second: 90 +}; + @Component({ selector: 'app-time', templateUrl: './time.component.html', @@ -12,19 +34,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { interval: number; text: string; tooltip: string; - precisionThresholds = { - year: 100, - month: 18, - week: 12, - day: 31, - hour: 48, - minute: 90, - second: 90 - }; - intervals = {}; @Input() time: number; - @Input() dateString: number; + @Input() dateString: string; @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain'; @Input() fastRender = false; @Input() fixedRender = false; @@ -40,37 +52,25 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { constructor( private ref: ChangeDetectorRef, private stateService: StateService, - private datePipe: DatePipe, - ) { - this.intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - second: 1 - }; - } + ) {} ngOnInit() { + this.calculateTime(); if(this.fixedRender){ - this.text = this.calculate(); return; } if (!this.stateService.isBrowser) { - this.text = this.calculate(); this.ref.markForCheck(); return; } this.interval = window.setInterval(() => { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); }, 1000 * (this.fastRender ? 1 : 60)); } ngOnChanges() { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); } @@ -78,40 +78,71 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { clearInterval(this.interval); } - calculate() { - if (this.time == null) { - return; + calculateTime(): void { + const { text, tooltip } = TimeComponent.calculate( + this.time, + this.kind, + this.relative, + this.precision, + this.minUnit, + this.showTooltip, + this.units, + this.dateString, + this.lowercaseStart, + this.numUnits, + this.fractionDigits, + ); + this.text = text; + this.tooltip = tooltip; + } + + static calculate( + time: number, + kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', + relative: boolean = false, + precision: number = 0, + minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', + showTooltip: boolean = false, + units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], + dateString?: string, + lowercaseStart: boolean = false, + numUnits: number = 1, + fractionDigits: number = 0, + ): { text: string, tooltip: string } { + if (time == null) { + return { text: '', tooltip: '' }; } let seconds: number; - switch (this.kind) { + let tooltip: string = ''; + switch (kind) { case 'since': - seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000); - this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm'); + seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); + tooltip = datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm'); break; case 'until': case 'within': - seconds = (+new Date(this.time) - +new Date()) / 1000; - this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm'); + seconds = (+new Date(time) - +new Date()) / 1000; + tooltip = datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm'); break; default: - seconds = Math.floor(this.time); - this.tooltip = ''; + seconds = Math.floor(time); + tooltip = ''; } - if (!this.showTooltip || this.relative) { - this.tooltip = ''; + if (!showTooltip || relative) { + tooltip = ''; } - if (seconds < 1 && this.kind === 'span') { - return $localize`:@@date-base.immediately:Immediately`; + if (seconds < 1 && kind === 'span') { + return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; } else if (seconds < 60) { - if (this.relative || this.kind === 'since') { - if (this.lowercaseStart) { - return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1); + if (relative || kind === 'since') { + if (lowercaseStart) { + return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; } - return $localize`:@@date-base.just-now:Just now`; - } else if (this.kind === 'until' || this.kind === 'within') { + return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; + } else if (kind === 'until' || kind === 'within') { seconds = 60; } } @@ -119,44 +150,44 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { let counter: number; const result = []; let usedUnits = 0; - for (const [index, unit] of this.units.entries()) { - let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)]; - counter = Math.floor(seconds / this.intervals[unit]); - const precisionCounter = Math.round(seconds / this.intervals[precisionUnit]); - if (precisionCounter > this.precisionThresholds[precisionUnit]) { + for (const [index, unit] of units.entries()) { + let precisionUnit = units[Math.min(units.length - 1, index + precision)]; + counter = Math.floor(seconds / intervals[unit]); + const precisionCounter = Math.round(seconds / intervals[precisionUnit]); + if (precisionCounter > precisionThresholds[precisionUnit]) { precisionUnit = unit; } - if (this.units.indexOf(precisionUnit) === this.units.indexOf(this.minUnit)) { + if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { counter = Math.max(1, counter); } if (counter > 0) { let rounded; - const roundFactor = Math.pow(10,this.fractionDigits || 0); - if ((this.kind === 'until' || this.kind === 'within') && usedUnits < this.numUnits) { - rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; + const roundFactor = Math.pow(10,fractionDigits || 0); + if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { + rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; } else { - rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; + rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; } - if ((this.kind !== 'until' && this.kind !== 'within')|| this.numUnits === 1) { - return this.formatTime(this.kind, precisionUnit, rounded); + if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { + return { tooltip, text: TimeComponent.formatTime(kind, precisionUnit, rounded) }; } else { if (!usedUnits) { - result.push(this.formatTime(this.kind, precisionUnit, rounded)); + result.push(TimeComponent.formatTime(kind, precisionUnit, rounded)); } else { - result.push(this.formatTime('', precisionUnit, rounded)); + result.push(TimeComponent.formatTime('', precisionUnit, rounded)); } - seconds -= (rounded * this.intervals[precisionUnit]); + seconds -= (rounded * intervals[precisionUnit]); usedUnits++; - if (usedUnits >= this.numUnits) { - return result.join(', '); + if (usedUnits >= numUnits) { + return { tooltip, text: result.join(', ') }; } } } } - return result.join(', '); + return { tooltip, text: result.join(', ') }; } - private formatTime(kind, unit, number): string { + static formatTime(kind, unit, number): string { const dateStrings = dates(number); switch (kind) { case 'since': diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index 5e034a700..91dc70240 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -6,6 +6,14 @@ 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'; +import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils'; +import { TimeComponent } from '../time/time.component'; + +const newColorHex = '1bd8f4'; +const oldColorHex = '9339f4'; +const pendingColorHex = 'eba814'; +const newColor = hexToColor(newColorHex); +const oldColor = hexToColor(oldColorHex); @Component({ selector: 'app-utxo-graph', @@ -29,7 +37,8 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { @Input() widget: boolean = false; subscription: Subscription; - redraw$: BehaviorSubject = new BehaviorSubject(false); + lastUpdate: number = 0; + updateInterval; chartOptions: EChartsOption = {}; chartInitOptions = { @@ -46,7 +55,14 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { private zone: NgZone, private router: Router, private relativeUrlPipe: RelativeUrlPipe, - ) {} + ) { + // re-render the chart every 10 seconds, to keep the age colors up to date + this.updateInterval = setInterval(() => { + if (this.lastUpdate < Date.now() - 10000 && this.utxos) { + this.prepareChartOptions(this.utxos); + } + }, 10000); + } ngOnChanges(changes: SimpleChanges): void { this.isLoading = true; @@ -82,7 +98,18 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { // 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); + const sortedUtxos = utxos.sort((a, b) => { + if (a.value === b.value) { + if (a.status.confirmed && !b.status.confirmed) { + return -1; + } else if (!a.status.confirmed && b.status.confirmed) { + return 1; + } else { + return a.status.block_height - b.status.block_height; + } + } + return b.value - a.value; + }).slice(0, 500); let centerOfMass = { x: 0, y: 0 }; let weightOfMass = 0; sortedUtxos.forEach((utxo, index) => { @@ -192,7 +219,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { const x = datum[2] as number; const y = datum[3] as number; const r = datum[4] as number; - if (r * scale < 3) { + if (r * scale < 2) { // skip items too small to render cleanly return; } @@ -207,7 +234,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { r: (r * scale) - 1, }, style: { - fill: '#5470c6', + fill: '#' + this.getColor(utxo), } }, ]; @@ -230,7 +257,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { type: 'group', children: elements, }; - } + }, }], tooltip: { backgroundColor: 'rgba(17, 19, 31, 1)', @@ -247,14 +274,40 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { return ` ${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}
- ${valueStr}`; + ${valueStr} +
+ ${utxo.status.confirmed ? 'Confirmed ' + TimeComponent.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} + `; }, } }; + this.lastUpdate = Date.now(); this.cd.markForCheck(); } + getColor(utxo: Utxo): string { + if (utxo.status.confirmed) { + const age = Date.now() / 1000 - utxo.status.block_time; + const oneHour = 60 * 60; + const fourYears = 4 * 365 * 24 * 60 * 60; + + if (age < oneHour) { + return newColorHex; + } else if (age >= fourYears) { + return oldColorHex; + } else { + // Logarithmic scale between 1 hour and 4 years + const logAge = Math.log(age / oneHour); + const logMax = Math.log(fourYears / oneHour); + const t = logAge / logMax; + return colorToHex(mix(newColor, oldColor, t)); + } + } else { + return pendingColorHex; + } + } + onChartClick(e): void { if (e.data?.[0]?.txid) { this.zone.run(() => { @@ -277,6 +330,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } + clearInterval(this.updateInterval); } isMobile(): boolean { diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 6bdc3262b..5ccb369f6 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -204,12 +204,12 @@ export function renderSats(value: number, network: string, mode: 'sats' | 'btc' break; } if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) { - return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`; + return `${amountShortenerPipe.transform(value / 100000000, 2)} ${prefix}BTC`; } else { if (prefix.length) { prefix += '-'; } - return `${amountShortenerPipe.transform(value)} ${prefix}sats`; + return `${amountShortenerPipe.transform(value, 2)} ${prefix}sats`; } } From e144e139b70bec3af496725a88706d41179912d6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Sep 2024 18:06:55 +0000 Subject: [PATCH 074/102] Update accelerating pie chart in real time --- .../active-acceleration-box.component.html | 4 ++-- .../active-acceleration-box.component.ts | 12 ++++++++---- .../transaction/transaction.component.html | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html index 13d38443e..dbc79fb95 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -10,10 +10,10 @@
- @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { + @if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { } @else { - + }
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 f95bb71c8..fb727c1a4 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 @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Transaction } from '../../../interfaces/electrs.interface'; import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; @@ -23,7 +23,8 @@ function toRGB({r,g,b}): string { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ActiveAccelerationBox implements OnChanges { - @Input() tx: Transaction; + @Input() acceleratedBy?: number[]; + @Input() effectiveFeeRate?: number; @Input() accelerationInfo: Acceleration; @Input() miningStats: MiningStats; @Input() pools: number[]; @@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges { timespan = ''; chartInstance: any = undefined; - constructor() {} + constructor( + private cd: ChangeDetectorRef, + ) {} ngOnChanges(changes: SimpleChanges): void { - const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy; + const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy; if (pools && this.miningStats) { this.prepareChartOptions(pools); } @@ -132,6 +135,7 @@ export class ActiveAccelerationBox implements OnChanges { } ] }; + this.cd.markForCheck(); } onChartInit(ec) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index c0f5c6103..9d3c0d678 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -670,7 +670,7 @@ - + From 4220f99477f8e337478e68c5f647dd3c6228f083 Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Sun, 22 Sep 2024 14:46:53 -0700 Subject: [PATCH 075/102] remove 'on'/UI changes per feedback --- backend/src/utils/bitcoin-script.ts | 3 ++- .../app/components/block/block.component.html | 1 - .../app/components/block/block.component.scss | 6 ------ .../blockchain-blocks.component.html | 21 +++++++++++-------- .../blockchain-blocks.component.scss | 17 ++++++++------- .../blockchain/blockchain.component.scss | 2 +- .../transaction/transaction.component.html | 1 - .../transaction/transaction.component.scss | 6 ------ .../app/dashboard/dashboard.component.scss | 2 +- 9 files changed, 25 insertions(+), 34 deletions(-) diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 619f1876d..b43b7a72d 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -220,7 +220,8 @@ export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null const tagStart = tagLengthByte + 1; const tags = bytes.slice(tagStart, tagStart + tagsLength); - const tagString = String.fromCharCode(...tags); + let tagString = String.fromCharCode(...tags); + tagString = tagString.replace('\x00', ''); return tagString.split('\x0f'); } \ No newline at end of file diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index d97ebafc5..46900179b 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -185,7 +185,6 @@
{{ block.extras.pool.minerNames[1] }}
- on {{ block.extras.pool.name }}
diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index 887d7281f..6eae3fe3a 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -94,12 +94,6 @@ h1 { border-radius: .25rem; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .pool-logo { width: 25px; height: 25px; diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 3fdafb540..79b9cea62 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -61,18 +61,21 @@
-
- {{ block.extras.pool.minerNames[1] }} - diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index b03b3d3cb..a0111215a 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -19,12 +19,6 @@ pointer-events: none; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .on-pool-name-text { display: inline-block; padding-top: 2px; @@ -42,10 +36,17 @@ } .on-pool-container { + align-items: center; + position: relative; + top: -8px; display: flex; flex-direction: column; } +.on-pool-container.selected { + top: 0px; +} + .pool-container { margin-top: 12px; } @@ -156,7 +157,7 @@ #arrow-up { position: relative; left: calc(var(--block-size) * 0.6); - top: calc(var(--block-size) * 1.38); + top: calc(var(--block-size) * 1.28); width: 0; height: 0; border-left: calc(var(--block-size) * 0.2) solid transparent; @@ -186,7 +187,7 @@ .badge { position: relative; - top: 8px; + top: 15px; z-index: 101; color: #FFF; } diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 7f98f5ed1..32225598a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -14,7 +14,7 @@ } .blockchain-wrapper { - height: 272px; + height: 260px; -webkit-user-select: none; /* Safari */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+/Edge */ diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index d00ab0e02..32eb10f8e 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -687,7 +687,6 @@
{{ pool.minerNames[1] }}
- on {{ pool.name }}
diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 43cece726..40b813cae 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -73,12 +73,6 @@ border-radius: .25rem; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .pool-logo { width: 25px; height: 25px; diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 0864f0096..9ad09981f 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -1,6 +1,6 @@ .dashboard-container { text-align: center; - margin-top: 1.0rem; + margin-top: 0.5rem; .col { margin-bottom: 1.5rem; } From 0e5698955fef808b8e66e86176abefcfff3842b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 02:09:59 +0000 Subject: [PATCH 076/102] Bump esbuild from 0.23.0 to 0.24.0 in /frontend Bumps [esbuild](https://github.com/evanw/esbuild) from 0.23.0 to 0.24.0. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: esbuild dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 398 ++++++++++++++++++------------------- frontend/package.json | 2 +- 2 files changed, 200 insertions(+), 200 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b53f80c88..2b10d398a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,7 @@ "clipboard": "^2.0.11", "domino": "^2.1.6", "echarts": "~5.5.0", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "lightweight-charts": "~3.8.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", @@ -3201,9 +3201,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], @@ -3216,9 +3216,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -3231,9 +3231,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -3246,9 +3246,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -3261,9 +3261,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -3276,9 +3276,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -3291,9 +3291,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -3306,9 +3306,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -3321,9 +3321,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -3336,9 +3336,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -3351,9 +3351,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -3366,9 +3366,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -3381,9 +3381,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -3396,9 +3396,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -3411,9 +3411,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -3426,9 +3426,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -3441,9 +3441,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -3456,9 +3456,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -3471,9 +3471,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ "arm64" ], @@ -3486,9 +3486,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -3501,9 +3501,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -3516,9 +3516,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -3531,9 +3531,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -3546,9 +3546,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -9210,9 +9210,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -9221,30 +9221,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/esbuild-wasm": { @@ -20616,147 +20616,147 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" }, "@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "optional": true }, "@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "optional": true }, "@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "optional": true }, "@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "optional": true }, "@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "optional": true }, "@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "optional": true }, "@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "optional": true }, "@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "optional": true }, "@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "optional": true }, "@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "optional": true }, "@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "optional": true }, "@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "optional": true }, "@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "optional": true }, "@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "optional": true }, "@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "optional": true }, "@eslint-community/eslint-utils": { @@ -25088,34 +25088,34 @@ } }, "esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "requires": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "esbuild-wasm": { diff --git a/frontend/package.json b/frontend/package.json index 3b5d61be0..415ac74fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -92,7 +92,7 @@ "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "tinyify": "^4.0.0", "tlite": "^0.1.9", "tslib": "~2.7.0", From 05e88a25be1f4ce4a1bba9f2229cdaed0af64506 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 23 Sep 2024 14:15:00 +0800 Subject: [PATCH 077/102] npm audit fix --- frontend/package-lock.json | 211 ++++++++++--------------------------- 1 file changed, 55 insertions(+), 156 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b10d398a..9d4e018ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8884,9 +8884,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "devOptional": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -8897,60 +8897,30 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, - "node_modules/engine.io-client/node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true, "engines": { "node": ">=10.0.0" @@ -8965,27 +8935,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -13406,9 +13355,9 @@ "optional": true }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "dependencies": { "isarray": "0.0.1" @@ -15952,33 +15901,13 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-client": { @@ -17860,12 +17789,12 @@ } }, "node_modules/wait-on/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -18337,9 +18266,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -24836,9 +24765,9 @@ } }, "engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "devOptional": true, "requires": { "@types/cookie": "^0.4.1", @@ -24849,8 +24778,8 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "cookie": { @@ -24858,48 +24787,26 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} } } }, "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } } }, "engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true }, "enhanced-resolve": { @@ -28210,9 +28117,9 @@ "optional": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "requires": { "isarray": "0.0.1" @@ -30070,21 +29977,13 @@ } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "requires": { - "ws": "~8.11.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "socket.io-client": { @@ -31339,12 +31238,12 @@ }, "dependencies": { "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -31674,9 +31573,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xhr2": { From 2a9346f695ea52608edb6cda26f9b331281248ef Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 23 Sep 2024 14:47:57 +0200 Subject: [PATCH 078/102] Don't show negative timespans on timeline --- .../acceleration-timeline.component.html | 4 ++-- .../acceleration-timeline.component.ts | 17 +++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html index 560e54629..ba0d44884 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html @@ -38,7 +38,7 @@
- +
@@ -46,7 +46,7 @@
@if (tx.status.confirmed) {
- +
} @else if (standardETA && !tx.status.confirmed) { diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts index da0eee4a3..16fd24c7f 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -24,6 +24,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { accelerateRatio: number; useAbsoluteTime: boolean = false; interval: number; + firstSeenToAccelerated: number; + acceleratedToMined: number; tooltipPosition = null; hoverInfo: any = null; @@ -35,8 +37,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { ngOnInit(): void { this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; - this.now = Math.floor(new Date().getTime() / 1000); - this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; this.miningService.getPools().subscribe(pools => { for (const pool of pools) { @@ -44,10 +44,8 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { } }); - this.interval = window.setInterval(() => { - this.now = Math.floor(new Date().getTime() / 1000); - this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; - }, 60000); + this.updateTimes(); + this.interval = window.setInterval(this.updateTimes.bind(this), 60000); } ngOnChanges(changes): void { @@ -64,6 +62,13 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { // } } + updateTimes(): void { + this.now = Math.floor(new Date().getTime() / 1000); + this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600; + this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime); + this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt); + } + ngOnDestroy(): void { clearInterval(this.interval); } From e6dbde952eaa19685007e14261bdf44e80f872cc Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Mon, 23 Sep 2024 12:36:10 -0700 Subject: [PATCH 079/102] Strip non-alphanumeric chars from miner names --- .../blockchain-blocks.component.html | 13 ++----------- .../blockchain-blocks.component.ts | 1 + 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 79b9cea62..128d18774 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -62,17 +62,8 @@
-
- {{ block.extras.pool.minerNames[1] }} -
- - {{ block.extras.pool.name }} -
-
- - - {{ block.extras.pool.minerNames[1] }} - + + {{ block.extras.pool.minerNames[1] }}
{{ block.extras.pool.name }} diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 512886f23..7846b66a2 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -283,6 +283,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { block.extras.maxFee = this.getMaxBlockFee(block); if (block.extras.pool?.minerNames) { block.extras.pool.minerNames = block.extras.pool.minerNames.map((name) => { + name = name.replace(/[^a-zA-Z0-9 ]/g, ''); if (name.length > 16) { return name.slice(0, 16) + '…'; } From 9984621e5e3f8cede57c8d862d6b3b37122cac91 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 24 Sep 2024 15:33:08 +0000 Subject: [PATCH 080/102] refactor static time formatting into new service --- frontend/src/app/app.module.ts | 2 + .../src/app/components/time/time.component.ts | 262 +----------------- .../utxo-graph/utxo-graph.component.ts | 6 +- 3 files changed, 9 insertions(+), 261 deletions(-) diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 50bbd88b9..d1129a602 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -21,6 +21,7 @@ import { StorageService } from './services/storage.service'; import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { LanguageService } from './services/language.service'; import { ThemeService } from './services/theme.service'; +import { TimeService } from './services/time.service'; import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; @@ -42,6 +43,7 @@ const providers = [ EnterpriseService, LanguageService, ThemeService, + TimeService, ShortenStringPipe, FiatShortenerPipe, FiatCurrencyPipe, diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index f0c73c80b..6360bca4a 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -1,29 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core'; import { StateService } from '../../services/state.service'; -import { dates } from '../../shared/i18n/dates'; -import { DatePipe } from '@angular/common'; - -const datePipe = new DatePipe(navigator.language || 'en-US'); - -const intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - second: 1 -}; - -const precisionThresholds = { - year: 100, - month: 18, - week: 12, - day: 31, - hour: 48, - minute: 90, - second: 90 -}; +import { TimeService } from '../../services/time.service'; @Component({ selector: 'app-time', @@ -52,6 +29,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { constructor( private ref: ChangeDetectorRef, private stateService: StateService, + private timeService: TimeService, ) {} ngOnInit() { @@ -79,7 +57,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { } calculateTime(): void { - const { text, tooltip } = TimeComponent.calculate( + const { text, tooltip } = this.timeService.calculate( this.time, this.kind, this.relative, @@ -95,238 +73,4 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { this.text = text; this.tooltip = tooltip; } - - static calculate( - time: number, - kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', - relative: boolean = false, - precision: number = 0, - minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', - showTooltip: boolean = false, - units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], - dateString?: string, - lowercaseStart: boolean = false, - numUnits: number = 1, - fractionDigits: number = 0, - ): { text: string, tooltip: string } { - if (time == null) { - return { text: '', tooltip: '' }; - } - - let seconds: number; - let tooltip: string = ''; - switch (kind) { - case 'since': - seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); - tooltip = datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm'); - break; - case 'until': - case 'within': - seconds = (+new Date(time) - +new Date()) / 1000; - tooltip = datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm'); - break; - default: - seconds = Math.floor(time); - tooltip = ''; - } - - if (!showTooltip || relative) { - tooltip = ''; - } - - if (seconds < 1 && kind === 'span') { - return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; - } else if (seconds < 60) { - if (relative || kind === 'since') { - if (lowercaseStart) { - return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; - } - return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; - } else if (kind === 'until' || kind === 'within') { - seconds = 60; - } - } - - let counter: number; - const result = []; - let usedUnits = 0; - for (const [index, unit] of units.entries()) { - let precisionUnit = units[Math.min(units.length - 1, index + precision)]; - counter = Math.floor(seconds / intervals[unit]); - const precisionCounter = Math.round(seconds / intervals[precisionUnit]); - if (precisionCounter > precisionThresholds[precisionUnit]) { - precisionUnit = unit; - } - if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { - counter = Math.max(1, counter); - } - if (counter > 0) { - let rounded; - const roundFactor = Math.pow(10,fractionDigits || 0); - if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { - rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; - } else { - rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; - } - if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { - return { tooltip, text: TimeComponent.formatTime(kind, precisionUnit, rounded) }; - } else { - if (!usedUnits) { - result.push(TimeComponent.formatTime(kind, precisionUnit, rounded)); - } else { - result.push(TimeComponent.formatTime('', precisionUnit, rounded)); - } - seconds -= (rounded * intervals[precisionUnit]); - usedUnits++; - if (usedUnits >= numUnits) { - return { tooltip, text: result.join(', ') }; - } - } - } - } - return { tooltip, text: result.join(', ') }; - } - - static formatTime(kind, unit, number): string { - const dateStrings = dates(number); - switch (kind) { - case 'since': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; - case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; - case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; - case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; - case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; - case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; - case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; - } - } - break; - case 'until': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'within': - if (number === 1) { - switch (unit) { // singular (In ~1 day) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`; - } - } else { - switch (unit) { // plural (In ~2 days) - case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'span': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; - case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; - case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; - case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; - case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; - case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; - case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; - } - } - break; - case 'before': - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYear}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonth}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeek}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDay}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHour}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinute}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSecond}:DATE: before`; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return $localize`:@@time-before:${dateStrings.i18nYears}:DATE: before`; break; - case 'month': return $localize`:@@time-before:${dateStrings.i18nMonths}:DATE: before`; break; - case 'week': return $localize`:@@time-before:${dateStrings.i18nWeeks}:DATE: before`; break; - case 'day': return $localize`:@@time-before:${dateStrings.i18nDays}:DATE: before`; break; - case 'hour': return $localize`:@@time-before:${dateStrings.i18nHours}:DATE: before`; break; - case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinutes}:DATE: before`; break; - case 'second': return $localize`:@@time-before:${dateStrings.i18nSeconds}:DATE: before`; break; - } - } - break; - default: - if (number === 1) { - switch (unit) { // singular (1 day) - case 'year': return dateStrings.i18nYear; break; - case 'month': return dateStrings.i18nMonth; break; - case 'week': return dateStrings.i18nWeek; break; - case 'day': return dateStrings.i18nDay; break; - case 'hour': return dateStrings.i18nHour; break; - case 'minute': return dateStrings.i18nMinute; break; - case 'second': return dateStrings.i18nSecond; break; - } - } else { - switch (unit) { // plural (2 days) - case 'year': return dateStrings.i18nYears; break; - case 'month': return dateStrings.i18nMonths; break; - case 'week': return dateStrings.i18nWeeks; break; - case 'day': return dateStrings.i18nDays; break; - case 'hour': return dateStrings.i18nHours; break; - case 'minute': return dateStrings.i18nMinutes; break; - case 'second': return dateStrings.i18nSeconds; break; - } - } - } - } } diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index 91dc70240..310ff0356 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { EChartsOption } from '../../graphs/echarts'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { Utxo } from '../../interfaces/electrs.interface'; import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; @@ -8,6 +8,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi import { renderSats } from '../../shared/common.utils'; import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils'; import { TimeComponent } from '../time/time.component'; +import { TimeService } from '../../services/time.service'; const newColorHex = '1bd8f4'; const oldColorHex = '9339f4'; @@ -55,6 +56,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { private zone: NgZone, private router: Router, private relativeUrlPipe: RelativeUrlPipe, + private timeService: TimeService, ) { // re-render the chart every 10 seconds, to keep the age colors up to date this.updateInterval = setInterval(() => { @@ -276,7 +278,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy {
${valueStr}
- ${utxo.status.confirmed ? 'Confirmed ' + TimeComponent.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} + ${utxo.status.confirmed ? 'Confirmed ' + this.timeService.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} `; }, } From 9091fc92101ee5393a4ea0f50ae742ac62e5d268 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 24 Sep 2024 15:55:23 +0000 Subject: [PATCH 081/102] add missing time.service.ts file --- frontend/src/app/services/time.service.ts | 266 ++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 frontend/src/app/services/time.service.ts diff --git a/frontend/src/app/services/time.service.ts b/frontend/src/app/services/time.service.ts new file mode 100644 index 000000000..6f7978774 --- /dev/null +++ b/frontend/src/app/services/time.service.ts @@ -0,0 +1,266 @@ +import { Injectable } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { dates } from '../shared/i18n/dates'; + +const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + second: 1 +}; + +const precisionThresholds = { + year: 100, + month: 18, + week: 12, + day: 31, + hour: 48, + minute: 90, + second: 90 +}; + +@Injectable({ + providedIn: 'root' +}) +export class TimeService { + + constructor(private datePipe: DatePipe) {} + + calculate( + time: number, + kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', + relative: boolean = false, + precision: number = 0, + minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', + showTooltip: boolean = false, + units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], + dateString?: string, + lowercaseStart: boolean = false, + numUnits: number = 1, + fractionDigits: number = 0, + ): { text: string, tooltip: string } { + if (time == null) { + return { text: '', tooltip: '' }; + } + + let seconds: number; + let tooltip: string = ''; + switch (kind) { + case 'since': + seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); + tooltip = this.datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm') || ''; + break; + case 'until': + case 'within': + seconds = (+new Date(time) - +new Date()) / 1000; + tooltip = this.datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm') || ''; + break; + default: + seconds = Math.floor(time); + tooltip = ''; + } + + if (!showTooltip || relative) { + tooltip = ''; + } + + if (seconds < 1 && kind === 'span') { + return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; + } else if (seconds < 60) { + if (relative || kind === 'since') { + if (lowercaseStart) { + return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; + } + return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; + } else if (kind === 'until' || kind === 'within') { + seconds = 60; + } + } + + let counter: number; + const result: string[] = []; + let usedUnits = 0; + for (const [index, unit] of units.entries()) { + let precisionUnit = units[Math.min(units.length - 1, index + precision)]; + counter = Math.floor(seconds / intervals[unit]); + const precisionCounter = Math.round(seconds / intervals[precisionUnit]); + if (precisionCounter > precisionThresholds[precisionUnit]) { + precisionUnit = unit; + } + if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { + counter = Math.max(1, counter); + } + if (counter > 0) { + let rounded; + const roundFactor = Math.pow(10,fractionDigits || 0); + if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { + rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; + } else { + rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; + } + if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { + return { tooltip, text: this.formatTime(kind, precisionUnit, rounded) }; + } else { + if (!usedUnits) { + result.push(this.formatTime(kind, precisionUnit, rounded)); + } else { + result.push(this.formatTime('', precisionUnit, rounded)); + } + seconds -= (rounded * intervals[precisionUnit]); + usedUnits++; + if (usedUnits >= numUnits) { + return { tooltip, text: result.join(', ') }; + } + } + } + } + return { tooltip, text: result.join(', ') }; + } + + private formatTime(kind, unit, number): string { + const dateStrings = dates(number); + switch (kind) { + case 'since': + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break; + case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break; + case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break; + case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break; + case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break; + case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break; + case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break; + case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break; + case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break; + case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break; + case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break; + case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break; + case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break; + } + } + break; + case 'until': + if (number === 1) { + switch (unit) { // singular (In ~1 day) + case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break; + case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break; + case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break; + case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break; + case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break; + case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`; + case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`; + } + } else { + switch (unit) { // plural (In ~2 days) + case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break; + case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break; + case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break; + case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break; + case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break; + case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break; + case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break; + } + } + break; + case 'within': + if (number === 1) { + switch (unit) { // singular (In ~1 day) + case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYear}:DATE:`; break; + case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonth}:DATE:`; break; + case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeek}:DATE:`; break; + case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDay}:DATE:`; break; + case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHour}:DATE:`; break; + case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinute}:DATE:`; + case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSecond}:DATE:`; + } + } else { + switch (unit) { // plural (In ~2 days) + case 'year': return $localize`:@@time-within:within ~${dateStrings.i18nYears}:DATE:`; break; + case 'month': return $localize`:@@time-within:within ~${dateStrings.i18nMonths}:DATE:`; break; + case 'week': return $localize`:@@time-within:within ~${dateStrings.i18nWeeks}:DATE:`; break; + case 'day': return $localize`:@@time-within:within ~${dateStrings.i18nDays}:DATE:`; break; + case 'hour': return $localize`:@@time-within:within ~${dateStrings.i18nHours}:DATE:`; break; + case 'minute': return $localize`:@@time-within:within ~${dateStrings.i18nMinutes}:DATE:`; break; + case 'second': return $localize`:@@time-within:within ~${dateStrings.i18nSeconds}:DATE:`; break; + } + } + break; + case 'span': + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break; + case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break; + case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break; + case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break; + case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break; + case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break; + case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break; + case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break; + case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break; + case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break; + case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break; + case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break; + case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break; + } + } + break; + case 'before': + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return $localize`:@@time-before:${dateStrings.i18nYear}:DATE: before`; break; + case 'month': return $localize`:@@time-before:${dateStrings.i18nMonth}:DATE: before`; break; + case 'week': return $localize`:@@time-before:${dateStrings.i18nWeek}:DATE: before`; break; + case 'day': return $localize`:@@time-before:${dateStrings.i18nDay}:DATE: before`; break; + case 'hour': return $localize`:@@time-before:${dateStrings.i18nHour}:DATE: before`; break; + case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinute}:DATE: before`; break; + case 'second': return $localize`:@@time-before:${dateStrings.i18nSecond}:DATE: before`; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return $localize`:@@time-before:${dateStrings.i18nYears}:DATE: before`; break; + case 'month': return $localize`:@@time-before:${dateStrings.i18nMonths}:DATE: before`; break; + case 'week': return $localize`:@@time-before:${dateStrings.i18nWeeks}:DATE: before`; break; + case 'day': return $localize`:@@time-before:${dateStrings.i18nDays}:DATE: before`; break; + case 'hour': return $localize`:@@time-before:${dateStrings.i18nHours}:DATE: before`; break; + case 'minute': return $localize`:@@time-before:${dateStrings.i18nMinutes}:DATE: before`; break; + case 'second': return $localize`:@@time-before:${dateStrings.i18nSeconds}:DATE: before`; break; + } + } + break; + default: + if (number === 1) { + switch (unit) { // singular (1 day) + case 'year': return dateStrings.i18nYear; break; + case 'month': return dateStrings.i18nMonth; break; + case 'week': return dateStrings.i18nWeek; break; + case 'day': return dateStrings.i18nDay; break; + case 'hour': return dateStrings.i18nHour; break; + case 'minute': return dateStrings.i18nMinute; break; + case 'second': return dateStrings.i18nSecond; break; + } + } else { + switch (unit) { // plural (2 days) + case 'year': return dateStrings.i18nYears; break; + case 'month': return dateStrings.i18nMonths; break; + case 'week': return dateStrings.i18nWeeks; break; + case 'day': return dateStrings.i18nDays; break; + case 'hour': return dateStrings.i18nHours; break; + case 'minute': return dateStrings.i18nMinutes; break; + case 'second': return dateStrings.i18nSeconds; break; + } + } + } + return ''; + } +} From 83b60941743506d38fc9dbe6f318fb6533fce287 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 24 Sep 2024 23:30:24 +0000 Subject: [PATCH 082/102] optimize utxo graph layout algorithm, enable transitions --- .../utxo-graph/utxo-graph.component.ts | 187 ++++++++++-------- 1 file changed, 110 insertions(+), 77 deletions(-) diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index 310ff0356..b220ae6ab 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -7,7 +7,6 @@ import { Router } from '@angular/router'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { renderSats } from '../../shared/common.utils'; import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils'; -import { TimeComponent } from '../time/time.component'; import { TimeService } from '../../services/time.service'; const newColorHex = '1bd8f4'; @@ -16,6 +15,30 @@ const pendingColorHex = 'eba814'; const newColor = hexToColor(newColorHex); const oldColor = hexToColor(oldColorHex); +interface Circle { + x: number, + y: number, + r: number, + i: number, +} + +interface UtxoCircle extends Circle { + utxo: Utxo; +} + +function sortedInsert(positions: { c1: Circle, c2: Circle, d: number, p: number, side?: boolean }[], newPosition: { c1: Circle, c2: Circle, d: number, p: number }): void { + let left = 0; + let right = positions.length; + while (left < right) { + const mid = Math.floor((left + right) / 2); + if (positions[mid].p > newPosition.p) { + right = mid; + } else { + left = mid + 1; + } + } + positions.splice(left, 0, newPosition, {...newPosition, side: true }); +} @Component({ selector: 'app-utxo-graph', templateUrl: './utxo-graph.component.html', @@ -76,7 +99,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { } } - prepareChartOptions(utxos: Utxo[]) { + prepareChartOptions(utxos: Utxo[]): void { if (!utxos || utxos.length === 0) { return; } @@ -85,20 +108,21 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { // 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] - ]; + const intersection = (c1: Circle, c2: Circle, d: number, r: number, side: boolean): { x: number, y: number} => { + const d1 = c1.r + r; + const d2 = c2.r + r; + const a = (d1 * d1 - d2 * d2 + d * d) / (2 * d); + const h = Math.sqrt(d1 * d1 - a * a); + const x3 = c1.x + a * (c2.x - c1.x) / d; + const y3 = c1.y + a * (c2.y - c1.y) / d; + return side + ? { x: x3 + h * (c2.y - c1.y) / d, y: y3 - h * (c2.x - c1.x) / d } + : { x: x3 - h * (c2.y - c1.y) / d, y: y3 + h * (c2.x - c1.x) / d }; }; - // Naive algorithm to pack circles as tightly as possible without overlaps - const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = []; + // ~Linear algorithm to pack circles as tightly as possible without overlaps + const placedCircles: UtxoCircle[] = []; + const positions: { c1: Circle, c2: Circle, d: number, p: number, side?: boolean }[] = []; // Pack in descending order of value, and limit to the top 500 to preserve performance const sortedUtxos = utxos.sort((a, b) => { if (a.value === b.value) { @@ -112,78 +136,82 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { } return b.value - a.value; }).slice(0, 500); - let centerOfMass = { x: 0, y: 0 }; - let weightOfMass = 0; + const maxR = Math.sqrt(sortedUtxos.reduce((max, utxo) => Math.max(max, utxo.value), 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] }); + placedCircles.push({ x: 0, y: 0, r, utxo, i: index }); 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); + placedCircles.push({ x: c.r + r, y: 0, r, utxo, i: index }); + sortedInsert(positions, { c1: c, c2: placedCircles[1], d: c.r + r, p: 0 }); + return; + } + if (index === 2) { + const c = placedCircles[0]; + placedCircles.push({ x: -c.r - r, y: 0, r, utxo, i: index }); + sortedInsert(positions, { c1: c, c2: placedCircles[2], d: c.r + r, p: 0 }); return; } // The best position will be touching two other circles - // generate a list of candidate points by finding all such positions + // find the closest such position to the center of the graph // 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 + let newCircle: UtxoCircle = null; + while (positions.length > 0) { + const position = positions.shift(); + // if the circles are too far apart, skip + if (position.d > (position.c1.r + position.c2.r + r + r)) { + continue; + } + + const { x, y } = intersection(position.c1, position.c2, position.d, r, position.side); + if (isNaN(x) || isNaN(y)) { + // should never happen + continue; + } + + // check if the circle would overlap any other circles here + let valid = true; + const nearbyCircles: { c: UtxoCircle, d: number, s: number }[] = []; + for (let k = 0; k < numCircles; k++) { + const c = placedCircles[k]; + if (k === position.c1.i || k === position.c2.i) { + nearbyCircles.push({ c, d: c.r + r, s: 0 }); 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); - } + const d = distance(x, y, c.x, c.y); + if (d < (r + c.r)) { + valid = false; + break; + } else { + nearbyCircles.push({ c, d, s: d - c.r - r }); + } + } + if (valid) { + newCircle = { x, y, r, utxo, i: index }; + // add new positions to the candidate list + const nearest = nearbyCircles.sort((a, b) => a.s - b.s).slice(0, 5); + for (const n of nearest) { + if (n.d < (n.c.r + r + maxR + maxR)) { + sortedInsert(positions, { c1: newCircle, c2: n.c, d: n.d, p: distance((n.c.x + x) / 2, (n.c.y + y), 0, 0) }); } - if (valid) { - candidates.push([x, y, distances]); - } - }); + } + break; } } - - // 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]); + if (newCircle) { + placedCircles.push(newCircle); + } else { + // should never happen + return; } - 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 @@ -194,23 +222,26 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { const width = maxX - minX; const height = maxY - minY; - const data = placedCircles.map((circle, index) => [ + const data = placedCircles.map((circle) => [ + circle.utxo.txid + circle.utxo.vout, circle.utxo, - index, circle.x, circle.y, - circle.r + circle.r, ]); this.chartOptions = { series: [{ type: 'custom', coordinateSystem: undefined, - data, + data: data, + encode: { + itemName: 0, + x: 2, + y: 3, + r: 4, + }, 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); @@ -218,6 +249,9 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { const scaledHeight = height * scale; const offsetX = (chartWidth - scaledWidth) / 2 - minX * scale; const offsetY = (chartHeight - scaledHeight) / 2 - minY * scale; + + const datum = data[params.dataIndex]; + const utxo = datum[1] as Utxo; const x = datum[2] as number; const y = datum[3] as number; const r = datum[4] as number; @@ -225,14 +259,13 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { // 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: { @@ -240,12 +273,10 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { } }, ]; - const labelFontSize = Math.min(36, r * scale * 0.25); + const labelFontSize = Math.min(36, r * scale * 0.3); if (labelFontSize > 8) { elements.push({ type: 'text', - x: (x * scale) + offsetX, - y: (y * scale) + offsetY, style: { text: valueStr, fontSize: labelFontSize, @@ -257,6 +288,8 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { } return { type: 'group', + x: (x * scale) + offsetX, + y: (y * scale) + offsetY, children: elements, }; }, @@ -271,7 +304,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { }, borderColor: '#000', formatter: (params: any): string => { - const utxo = params.data[0] as Utxo; + const utxo = params.data[1] as Utxo; const valueStr = renderSats(utxo.value, this.stateService.network); return ` ${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout} From 2ad52e2c78225a4444db5d200883f1e96ea0a8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 02:37:00 +0000 Subject: [PATCH 083/102] Bump cypress from 13.14.0 to 13.15.0 in /frontend Bumps [cypress](https://github.com/cypress-io/cypress) from 13.14.0 to 13.15.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.14.0...v13.15.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 | 200 ++++++++++++------------------------- frontend/package.json | 2 +- 2 files changed, 66 insertions(+), 136 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9d4e018ef..af95a32d3 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.15.0", "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.24.0", @@ -62,7 +63,7 @@ "optionalDependencies": { "@cypress/schematic": "^2.5.0", "@types/cypress": "^1.1.3", - "cypress": "^13.14.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", @@ -3113,9 +3114,9 @@ } }, "node_modules/@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", "optional": true, "dependencies": { "aws-sign2": "~0.7.0", @@ -3124,14 +3125,14 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -5797,9 +5798,9 @@ } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "optional": true }, "node_modules/axios": { @@ -6065,20 +6066,6 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/qs": { - "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.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -8045,13 +8032,13 @@ "peer": true }, "node_modules/cypress": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", - "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", "hasInstallScript": true, "optional": true, "dependencies": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -9896,20 +9883,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/qs": { - "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.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10305,17 +10278,17 @@ } }, "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "optional": true, "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.12" + "node": ">= 6" } }, "node_modules/forwarded": { @@ -10957,14 +10930,14 @@ } }, "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "optional": true, "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" }, "engines": { "node": ">=0.10" @@ -14737,12 +14710,11 @@ } }, "node_modules/qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "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" @@ -16129,9 +16101,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "dependencies": { "asn1": "~0.2.3", @@ -16725,9 +16697,9 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "optional": true, "dependencies": { "psl": "^1.1.33", @@ -17799,20 +17771,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/wait-on/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/wait-on/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -20466,9 +20424,9 @@ } }, "@cypress/request": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", - "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", "optional": true, "requires": { "aws-sign2": "~0.7.0", @@ -20477,14 +20435,14 @@ "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "http-signature": "~1.3.6", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "6.10.4", + "qs": "6.13.0", "safe-buffer": "^5.1.2", "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", @@ -22369,9 +22327,9 @@ "optional": true }, "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", "optional": true }, "axios": { @@ -22583,14 +22541,6 @@ "requires": { "ee-first": "1.1.1" } - }, - "qs": { - "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.6" - } } } }, @@ -24100,12 +24050,12 @@ "peer": true }, "cypress": { - "version": "13.14.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.14.0.tgz", - "integrity": "sha512-r0+nhd033x883YL6068futewUsl02Q7rWiinyAAIBDW/OOTn+UMILWgNuCiY3vtJjd53efOqq5R9dctQk/rKiw==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", "optional": true, "requires": { - "@cypress/request": "^3.0.1", + "@cypress/request": "^3.0.4", "@cypress/xvfb": "^1.2.4", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", @@ -25554,14 +25504,6 @@ "ee-first": "1.1.1" } }, - "qs": { - "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.6" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -25853,13 +25795,13 @@ "optional": true }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "optional": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -26321,14 +26263,14 @@ } }, "http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "optional": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", - "sshpk": "^1.14.1" + "sshpk": "^1.18.0" } }, "https-browserify": { @@ -29098,12 +29040,11 @@ } }, "qs": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", - "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", - "optional": true, + "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" } }, "querystring": { @@ -30167,9 +30108,9 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "optional": true, "requires": { "asn1": "~0.2.3", @@ -30615,9 +30556,9 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", "optional": true, "requires": { "psl": "^1.1.33", @@ -31248,17 +31189,6 @@ "proxy-from-env": "^1.1.0" } }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 415ac74fe..3318d5031 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.14.0", + "cypress": "^13.15.0", "cypress-fail-on-console-error": "~5.1.0", "cypress-wait-until": "^2.0.1", "mock-socket": "~9.3.1", From b29c4cf228b6471597a8c61dbabd5b00c656ca23 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 24 Sep 2024 17:28:46 +0000 Subject: [PATCH 084/102] refactor miner name truncation --- backend/src/utils/bitcoin-script.ts | 2 +- .../block/block-preview.component.html | 38 +++++++++---------- .../app/components/block/block.component.html | 19 +++++----- .../app/components/block/block.component.scss | 14 ++----- .../blockchain-blocks.component.html | 6 +-- .../blockchain-blocks.component.scss | 9 ++++- .../blockchain-blocks.component.ts | 17 --------- .../transaction/transaction.component.html | 20 +++++----- .../transaction/transaction.component.scss | 14 ++----- 9 files changed, 53 insertions(+), 86 deletions(-) diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index b43b7a72d..f9755fcb4 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -223,5 +223,5 @@ export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null let tagString = String.fromCharCode(...tags); tagString = tagString.replace('\x00', ''); - return tagString.split('\x0f'); + return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, '')); } \ No newline at end of file diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html index b1cafc05e..036ab8399 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -53,32 +53,28 @@ Miner
- - {{ block.extras.pool.minerNames[1] }} -
- on - - {{ block.extras.pool.name}} -
-
- - - {{ block.extras.pool.name }} - + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + + + {{ block.extras.pool.name }}
- - {{ block?.extras.pool.minerNames[1] }} -
- on {{ block?.extras.pool.name }} -
-
- - {{ block?.extras.pool.name }} - + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + + {{ block.extras.pool.name }}
diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index 46900179b..09c3a5d23 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -182,16 +182,15 @@ Miner -
- {{ block.extras.pool.minerNames[1] }} -
- - {{ block.extras.pool.name }} -
-
- - {{ block.extras.pool.name }} - + + @if (block.extras.pool.minerNames[1].length > 16) { + {{ block.extras.pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ block.extras.pool.minerNames[1] }} + } + + + {{ block.extras.pool.name }}
diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index 6eae3fe3a..945d61366 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -81,17 +81,9 @@ h1 { } } -.on-pool-container { - display: inline; - flex-direction: row; -} - -.on-pool { - background-color: var(--bg); - display: inline-block; - margin-top: 4px; - padding: .25em .4em; - border-radius: .25rem; +.miner-name { + margin-right: 4px; + vertical-align: top; } .pool-logo { diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 128d18774..a782e9588 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -60,11 +60,11 @@
- - + {{ block.extras.pool.name }} diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index a0111215a..5c2a5ab5a 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -187,9 +187,16 @@ .badge { position: relative; - top: 15px; + top: 19px; z-index: 101; color: #FFF; + overflow: hidden; + text-overflow: ellipsis; + max-width: 145px; + + &.miner-name { + max-width: 125px; + } } .pool-logo { diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 7846b66a2..1a7598079 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -281,15 +281,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { if (block?.extras) { block.extras.minFee = this.getMinBlockFee(block); block.extras.maxFee = this.getMaxBlockFee(block); - if (block.extras.pool?.minerNames) { - block.extras.pool.minerNames = block.extras.pool.minerNames.map((name) => { - name = name.replace(/[^a-zA-Z0-9 ]/g, ''); - if (name.length > 16) { - return name.slice(0, 16) + '…'; - } - return name; - }); - } } } this.blocks.push(block || { @@ -332,14 +323,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { if (block?.extras) { block.extras.minFee = this.getMinBlockFee(block); block.extras.maxFee = this.getMaxBlockFee(block); - if (block.extras.pool?.minerNames) { - block.extras.pool.minerNames = block.extras.pool.minerNames.map((name) => { - if (name.length > 16) { - return name.slice(0, 16) + '…'; - } - return name; - }); - } } this.blocks[blockIndex] = block; this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex); diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index a4524d529..ec06dd5ad 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -684,17 +684,15 @@ @if (pool) { -
- {{ pool.minerNames[1] }} -
- - {{ pool.name }} -
-
- - - {{ pool.name }} - + + @if (pool.minerNames[1].length > 16) { + {{ pool.minerNames[1].slice(0, 15) }}… + } @else { + {{ pool.minerNames[1] }} + } + + + {{ pool.name }}
} @else { diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 40b813cae..42325a1b4 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -60,17 +60,9 @@ top: -1px; } -.on-pool-container { - display: inline; - flex-direction: row; -} - -.on-pool { - background-color: var(--bg); - display: inline-block; - margin-top: 4px; - padding: .25em .4em; - border-radius: .25rem; +.miner-name { + margin-right: 4px; + vertical-align: top; } .pool-logo { From 1d5843a112438c2f5ae2c12ea7949f04a3e175a8 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 26 Sep 2024 22:14:44 +0000 Subject: [PATCH 085/102] fix utxo chart on-click navigation --- .../src/app/components/utxo-graph/utxo-graph.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index b220ae6ab..3a549c1e7 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -344,13 +344,13 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { } onChartClick(e): void { - if (e.data?.[0]?.txid) { + if (e.data?.[1]?.txid) { this.zone.run(() => { - const url = this.relativeUrlPipe.transform(`/tx/${e.data[0].txid}`); + const url = this.relativeUrlPipe.transform(`/tx/${e.data[1].txid}`); if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { - window.open(url + '?mode=details#vout=' + e.data[0].vout); + window.open(url + '?mode=details#vout=' + e.data[1].vout); } else { - this.router.navigate([url], { fragment: `vout=${e.data[0].vout}` }); + this.router.navigate([url], { fragment: `vout=${e.data[1].vout}` }); } }); } From 2d7316942f2809ebd452d20e3c339605372f1160 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Fri, 27 Sep 2024 17:26:27 +0200 Subject: [PATCH 086/102] export bitcoinsatoshis pipe module, allow custom class for first part --- frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts | 4 ++-- frontend/src/app/shared/shared.module.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts index 7065b5138..7e785e9c8 100644 --- a/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts +++ b/frontend/src/app/shared/pipes/bitcoinsatoshis.pipe.ts @@ -8,7 +8,7 @@ export class BitcoinsatoshisPipe implements PipeTransform { constructor(private sanitizer: DomSanitizer) { } - transform(value: string): SafeHtml { + transform(value: string, firstPartClass?: string): SafeHtml { const newValue = this.insertSpaces(parseFloat(value || '0').toFixed(8)); const position = (newValue || '0').search(/[1-9]/); @@ -16,7 +16,7 @@ export class BitcoinsatoshisPipe implements PipeTransform { const secondPart = newValue.slice(position); return this.sanitizer.bypassSecurityTrustHtml( - `${firstPart}${secondPart}` + `${firstPart}${secondPart}` ); } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 0e37bc9d5..92b461548 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -365,6 +365,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir TwitterWidgetComponent, TwitterLogin, BitcoinInvoiceComponent, + BitcoinsatoshisPipe, MempoolBlockOverviewComponent, ClockchainComponent, From b26d26b14ca304a5a25629042d8a991d06be0c97 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 27 Sep 2024 15:55:29 +0000 Subject: [PATCH 087/102] expose custom x-total-count header --- production/nginx/location-api-v1-services.conf | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/production/nginx/location-api-v1-services.conf b/production/nginx/location-api-v1-services.conf index 88f510e79..a9df64bc6 100644 --- a/production/nginx/location-api-v1-services.conf +++ b/production/nginx/location-api-v1-services.conf @@ -92,6 +92,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin 'https://mempool.space'; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; # set CORS for approved hostnames @@ -100,6 +101,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin "$http_origin"; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; } @@ -108,6 +110,7 @@ location @mempool-api-v1-services-cache-disabled-addcors { add_header Access-Control-Allow-Origin "$cors_origin" always; add_header Access-Control-Allow-Headers "$cors_headers" always; add_header Access-Control-Allow-Credentials "$cors_credentials" always; + add_header Access-Control-Expose-Headers "$cors_expose_headers" always; proxy_redirect off; proxy_buffering off; @@ -172,6 +175,7 @@ location @mempool-api-v1-services-cache-short-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin 'https://mempool.space'; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; # set CORS for approved hostnames @@ -180,6 +184,7 @@ location @mempool-api-v1-services-cache-short-addcors { set $cors_methods 'GET, POST, PUT, DELETE, OPTIONS'; set $cors_origin "$http_origin"; set $cors_headers 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'; + set $cors_expose_headers 'X-Total-Count'; set $cors_credentials 'true'; } @@ -188,6 +193,7 @@ location @mempool-api-v1-services-cache-short-addcors { add_header Access-Control-Allow-Origin "$cors_origin" always; add_header Access-Control-Allow-Headers "$cors_headers" always; add_header Access-Control-Allow-Credentials "$cors_credentials" always; + add_header Access-Control-Expose-Headers "$cors_expose_headers" always; # add our own cache headers add_header 'Pragma' 'public'; From ea08c0c950831ea652283d930359dd56541eee2e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 27 Sep 2024 16:09:12 +0000 Subject: [PATCH 088/102] fix acceleration history paging w/ undefined total --- frontend/src/app/services/services-api.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 5213e131c..c87044781 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -165,7 +165,7 @@ export class ServicesApiServices { return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe( map((response) => ({ page, - total: parseInt(response.headers.get('X-Total-Count'), 10), + total: parseInt(response.headers.get('X-Total-Count'), 10) || 0, accelerations: accelerations.concat(response.body || []), })), switchMap(({page, total, accelerations}) => { From da2341dd00c57bbf5e304a18010399b9cbdc56a0 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 28 Sep 2024 08:56:29 +0400 Subject: [PATCH 089/102] remove rocket beta --- .../src/app/components/master-page/master-page.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 9fc2d4e58..1aa13e309 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -85,7 +85,6 @@