From 25482b9a0665ed0e673b5850eaccc9bae4d4eae5 Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Fri, 20 Sep 2024 14:31:31 -0700 Subject: [PATCH 01/29] show miner name on block timeline --- backend/src/api/blocks.ts | 6 +++ backend/src/mempool.interfaces.ts | 1 + backend/src/repositories/BlocksRepository.ts | 6 +++ backend/src/utils/bitcoin-script.ts | 23 +++++++++++ .../block/block-preview.component.html | 26 +++++++++++-- .../app/components/block/block.component.html | 13 ++++++- .../app/components/block/block.component.scss | 27 +++++++++++++ .../blockchain-blocks.component.html | 15 ++++++- .../blockchain-blocks.component.scss | 39 ++++++++++++++++++- .../blockchain-blocks.component.ts | 16 ++++++++ .../blockchain/blockchain.component.scss | 2 +- .../transaction/transaction.component.html | 14 ++++++- .../transaction/transaction.component.scss | 27 +++++++++++++ .../app/dashboard/dashboard.component.scss | 2 +- .../src/app/interfaces/node-api.interface.ts | 1 + 15 files changed, 204 insertions(+), 14 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 306179ca5..9a7d8b11a 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -34,6 +34,7 @@ import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp'; import mempool from './mempool'; import CpfpRepository from '../repositories/CpfpRepository'; import accelerationApi from './services/acceleration'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; class Blocks { private blocks: BlockExtended[] = []; @@ -342,7 +343,12 @@ class Blocks { id: pool.uniqueId, name: pool.name, slug: pool.slug, + minerNames: null, }; + + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } } extras.matchRate = null; diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index ccbc94bfa..6eee1a9ee 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -299,6 +299,7 @@ export interface BlockExtension { id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id` name: string; slug: string; + minerNames: string[] | null; }; avgFee: number; avgFeeRate: number; diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index de6c1deb8..f958e5c8b 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips'; import blocks from '../api/blocks'; import BlocksAuditsRepository from './BlocksAuditsRepository'; import transactionUtils from '../api/transaction-utils'; +import { parseDATUMTemplateCreator } from '../utils/bitcoin-script'; interface DatabaseBlock { id: string; @@ -1054,6 +1055,7 @@ class BlocksRepository { id: dbBlk.poolId, name: dbBlk.poolName, slug: dbBlk.poolSlug, + minerNames: null, }; extras.avgFee = dbBlk.avgFee; extras.avgFeeRate = dbBlk.avgFeeRate; @@ -1123,6 +1125,10 @@ class BlocksRepository { } } + if (extras.pool.name === 'OCEAN') { + extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw); + } + blk.extras = extras; return blk; } diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 8f551aa23..619f1876d 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -200,4 +200,27 @@ export function getVarIntLength(n: number): number { } else { return 9; } +} + +/** Extracts miner names from a DATUM coinbase transaction */ +export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null { + let bytes: number[] = []; + for (let c = 0; c < coinbaseRaw.length; c += 2) { + bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16)); + } + + // Skip block height + let tagLengthByte = 1 + bytes[0]; + + let tagsLength = bytes[tagLengthByte]; + if (tagsLength == 0x4c) { + tagLengthByte += 1; + tagsLength = bytes[tagLengthByte]; + } + + const tagStart = tagLengthByte + 1; + const tags = bytes.slice(tagStart, tagStart + tagsLength); + const tagString = String.fromCharCode(...tags); + + return tagString.split('\x0f'); } \ 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 56fa8886e..b1cafc05e 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -53,15 +53,33 @@ Miner - - {{ block.extras.pool.name }} + + {{ block.extras.pool.minerNames[1] }} +
+ on + + {{ block.extras.pool.name}} +
+
+ + + {{ block.extras.pool.name }} +
- {{ block?.extras.pool.name }} - + + {{ block?.extras.pool.minerNames[1] }} +
+ on {{ block?.extras.pool.name }} +
+
+ + {{ 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 1dd9d8a8d..d97ebafc5 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -182,8 +182,17 @@ Miner - - {{ block.extras.pool.name }} +
+ {{ block.extras.pool.minerNames[1] }} +
+ on + + {{ block.extras.pool.name }} +
+
+ + {{ 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 fe5318375..887d7281f 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -81,6 +81,33 @@ 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; +} + +.on-pool-text { + font-weight: normal; + color: gray; + padding-inline-end: 4px; +} + +.pool-logo { + width: 25px; + height: 25px; + position: relative; + top: -1px; + margin-right: 2px; +} + .row { flex-direction: column; @media (min-width: 768px) { 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 a60e1db0a..3fdafb540 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -61,8 +61,19 @@ 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 b8de4f2ca..b03b3d3cb 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -19,6 +19,37 @@ 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; + font-weight: normal; +} + + +.on-pool { + align-items: center; + background-color: var(--bg); + display: inline-block; + margin-top: 4px; + padding: .25em .4em; + border-radius: .25rem; +} + +.on-pool-container { + display: flex; + flex-direction: column; +} + +.pool-container { + margin-top: 12px; +} + .mined-block { position: absolute; top: 0px; @@ -125,7 +156,7 @@ #arrow-up { position: relative; left: calc(var(--block-size) * 0.6); - top: calc(var(--block-size) * 1.28); + top: calc(var(--block-size) * 1.38); width: 0; height: 0; border-left: calc(var(--block-size) * 0.2) solid transparent; @@ -155,7 +186,7 @@ .badge { position: relative; - top: 15px; + top: 8px; z-index: 101; color: #FFF; } @@ -168,6 +199,10 @@ margin-right: 2px; } +.pool-logo.faded { + filter: grayscale(100%) brightness(1.5); +} + .animated { transition: all 0.15s ease-in-out; white-space: nowrap; 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 1a7598079..512886f23 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -281,6 +281,14 @@ 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.push(block || { @@ -323,6 +331,14 @@ 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/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 32225598a..7f98f5ed1 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: 260px; + height: 272px; -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 b2e55a3b0..d00ab0e02 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -684,8 +684,18 @@ @if (pool) { - - {{ pool.name }} +
+ {{ pool.minerNames[1] }} +
+ on + + {{ pool.name }} +
+
+ + + {{ pool.name }} +
} @else { diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 1706dfcab..43cece726 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -60,6 +60,33 @@ 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; +} + +.on-pool-text { + font-weight: normal; + color: gray; + padding-inline-end: 4px; +} + +.pool-logo { + width: 25px; + height: 25px; + position: relative; + top: -1px; + margin-right: 2px; +} + .badge.badge-accelerated { background-color: var(--tertiary); color: white; diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 9ad09981f..0864f0096 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: 0.5rem; + margin-top: 1.0rem; .col { margin-bottom: 1.5rem; } diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 3e38ff88b..4c7796590 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -203,6 +203,7 @@ export interface BlockExtension { id: number; name: string; slug: string; + minerNames: string[] | null; } } From b90cd4c7e3bf8840fead9fa3860629f831c74eba Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Fri, 20 Sep 2024 14:59:21 -0700 Subject: [PATCH 02/29] restore minerNames property on pool --- 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 b61668f18..1306c432d 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -42,6 +42,7 @@ interface Pool { id: number; name: string; slug: string; + minerNames: string[] | null; } export interface TxAuditStatus { From 06e699e52b38b051712a2f4775d37887c808bb3a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Sep 2024 16:49:08 +0000 Subject: [PATCH 03/29] 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 4220f99477f8e337478e68c5f647dd3c6228f083 Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Sun, 22 Sep 2024 14:46:53 -0700 Subject: [PATCH 04/29] 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 2a9346f695ea52608edb6cda26f9b331281248ef Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 23 Sep 2024 14:47:57 +0200 Subject: [PATCH 05/29] 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 06/29] 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 07/29] 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 08/29] 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 09/29] 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 10/29] 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 11/29] 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 12/29] 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 13/29] 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 14/29] 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 15/29] 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 16/29] 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 @@