From fbf27560b3eca06504994cca534799f27dd9f561 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 10 Aug 2024 13:53:49 +0000 Subject: [PATCH 01/94] optimize processNewBlocks --- backend/src/api/mempool-blocks.ts | 92 +++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index 5d9dcf8f4..6e547e653 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -369,7 +369,7 @@ class MempoolBlocks { const lastBlockIndex = blocks.length - 1; let hasBlockStack = blocks.length >= 8; let stackWeight; - let feeStatsCalculator: OnlineFeeStatsCalculator | void; + let feeStatsCalculator: OnlineFeeStatsCalculator | null = null; if (hasBlockStack) { if (blockWeights && blockWeights[7] !== null) { stackWeight = blockWeights[7]; @@ -380,28 +380,36 @@ class MempoolBlocks { feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]); } + const ancestors: Ancestor[] = []; + const descendants: Ancestor[] = []; + let ancestor: MempoolTransactionExtended for (const cluster of clusters) { for (const memberTxid of cluster) { const mempoolTx = mempool[memberTxid]; if (mempoolTx) { - const ancestors: Ancestor[] = []; - const descendants: Ancestor[] = []; + // ugly micro-optimization to avoid allocating new arrays + ancestors.length = 0; + descendants.length = 0; let matched = false; cluster.forEach(txid => { + ancestor = mempool[txid]; if (txid === memberTxid) { matched = true; } else { - if (!mempool[txid]) { + if (!ancestor) { console.log('txid missing from mempool! ', txid, candidates?.txs[txid]); + return; } const relative = { txid: txid, - fee: mempool[txid].fee, - weight: (mempool[txid].adjustedVsize * 4), + fee: ancestor.fee, + weight: (ancestor.adjustedVsize * 4), }; if (matched) { descendants.push(relative); - mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0); + if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) { + mempoolTx.lastBoosted = ancestor.firstSeen; + } } else { ancestors.push(relative); } @@ -410,7 +418,20 @@ class MempoolBlocks { if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) { mempoolTx.cpfpDirty = true; } - Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true}); + // ugly micro-optimization to avoid allocating new arrays or objects + if (mempoolTx.ancestors) { + mempoolTx.ancestors.length = 0; + } else { + mempoolTx.ancestors = []; + } + if (mempoolTx.descendants) { + mempoolTx.descendants.length = 0; + } else { + mempoolTx.descendants = []; + } + mempoolTx.ancestors.push(...ancestors); + mempoolTx.descendants.push(...descendants); + mempoolTx.cpfpChecked = true; } } } @@ -420,7 +441,10 @@ class MempoolBlocks { const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2; // update this thread's mempool with the results let mempoolTx: MempoolTransactionExtended; - const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => { + let acceleration: Acceleration; + const mempoolBlocks: MempoolBlockWithTransactions[] = []; + for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) { + const block = blocks[blockIndex]; let totalSize = 0; let totalVsize = 0; let totalWeight = 0; @@ -436,7 +460,8 @@ class MempoolBlocks { } } - for (const txid of block) { + for (let i = 0; i < block.length; i++) { + const txid = block[i]; if (txid) { mempoolTx = mempool[txid]; // save position in projected blocks @@ -445,30 +470,37 @@ class MempoolBlocks { vsize: totalVsize + (mempoolTx.vsize / 2), }; - const acceleration = accelerations[txid]; - if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { - if (!mempoolTx.acceleration) { - mempoolTx.cpfpDirty = true; - } - mempoolTx.acceleration = true; - mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; - mempoolTx.acceleratedAt = acceleration?.added; - mempoolTx.feeDelta = acceleration?.feeDelta; - for (const ancestor of mempoolTx.ancestors || []) { - if (!mempool[ancestor.txid].acceleration) { - mempool[ancestor.txid].cpfpDirty = true; + if (txid in accelerations) { + acceleration = accelerations[txid]; + if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) { + if (!mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + } + mempoolTx.acceleration = true; + mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools; + mempoolTx.acceleratedAt = acceleration?.added; + mempoolTx.feeDelta = acceleration?.feeDelta; + for (const ancestor of mempoolTx.ancestors || []) { + if (!mempool[ancestor.txid].acceleration) { + mempool[ancestor.txid].cpfpDirty = true; + } + mempool[ancestor.txid].acceleration = true; + mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; + mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; + mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; + isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; + } + } else { + if (mempoolTx.acceleration) { + mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - mempool[ancestor.txid].acceleration = true; - mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy; - mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt; - mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta; - isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy; } } else { if (mempoolTx.acceleration) { mempoolTx.cpfpDirty = true; + delete mempoolTx.acceleration; } - delete mempoolTx.acceleration; } // online calculation of stack-of-blocks fee stats @@ -486,7 +518,7 @@ class MempoolBlocks { } } } - return this.dataToMempoolBlocks( + mempoolBlocks[blockIndex] = this.dataToMempoolBlocks( block, transactions, totalSize, @@ -494,7 +526,7 @@ class MempoolBlocks { totalFees, (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined, ); - }); + }; if (saveResults) { const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks); From c7f48b4390a7973bb97b02b6cfe239d0b5feee8c Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 19 Aug 2024 16:29:34 +0200 Subject: [PATCH 02/94] Add amount mode selector to footer --- .../amount-selector.component.html | 7 ++++ .../amount-selector.component.scss | 0 .../amount-selector.component.ts | 36 +++++++++++++++++++ .../global-footer.component.html | 6 +++- .../global-footer.component.scss | 5 +++ frontend/src/app/shared/shared.module.ts | 3 ++ 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.html create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.scss create mode 100644 frontend/src/app/components/amount-selector/amount-selector.component.ts diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.html b/frontend/src/app/components/amount-selector/amount-selector.component.html new file mode 100644 index 000000000..b509d6fe3 --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.scss b/frontend/src/app/components/amount-selector/amount-selector.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.ts b/frontend/src/app/components/amount-selector/amount-selector.component.ts new file mode 100644 index 000000000..144b0f1db --- /dev/null +++ b/frontend/src/app/components/amount-selector/amount-selector.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { StorageService } from '../../services/storage.service'; +import { StateService } from '../../services/state.service'; + +@Component({ + selector: 'app-amount-selector', + templateUrl: './amount-selector.component.html', + styleUrls: ['./amount-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AmountSelectorComponent implements OnInit { + amountForm: UntypedFormGroup; + modes = ['btc', 'sats', 'fiat']; + + constructor( + private formBuilder: UntypedFormBuilder, + private stateService: StateService, + private storageService: StorageService, + ) { } + + ngOnInit() { + this.amountForm = this.formBuilder.group({ + mode: ['btc'] + }); + this.stateService.viewAmountMode$.subscribe((mode) => { + this.amountForm.get('mode')?.setValue(mode); + }); + } + + changeMode() { + const newMode = this.amountForm.get('mode')?.value; + this.storageService.setValue('view-amount-mode', newMode); + this.stateService.viewAmountMode$.next(newMode); + } +} diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index a2e7286e0..1765bc6fc 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -27,6 +27,9 @@
+
+ +
@if (!env.customize?.theme) {
@@ -39,7 +42,8 @@
@if (!env.customize?.theme) {
- + +
} @if (!enterpriseInfo?.footer_img) { diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index e0daf4f4c..b815da754 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -76,6 +76,11 @@ footer .selector { display: inline-block; } +footer .add-margin { + margin-left: 5px; + margin-right: 5px; +} + footer .row.link-tree { max-width: 1140px; margin: 0 auto; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 2d5b4d0f9..2e300a300 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -35,6 +35,7 @@ import { LanguageSelectorComponent } from '../components/language-selector/langu import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component'; import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component'; import { ThemeSelectorComponent } from '../components/theme-selector/theme-selector.component'; +import { AmountSelectorComponent } from '../components/amount-selector/amount-selector.component'; import { BrowserOnlyDirective } from './directives/browser-only.directive'; import { ServerOnlyDirective } from './directives/server-only.directive'; import { ColoredPriceDirective } from './directives/colored-price.directive'; @@ -131,6 +132,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, ThemeSelectorComponent, RateUnitSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, NoSanitizePipe, @@ -278,6 +280,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir FiatSelectorComponent, RateUnitSelectorComponent, ThemeSelectorComponent, + AmountSelectorComponent, ScriptpubkeyTypePipe, RelativeUrlPipe, Hex2asciiPipe, From e59308c2f5ab79d24e9a3f2fca7ecb135284fcac Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 19 Aug 2024 17:13:41 +0200 Subject: [PATCH 03/94] Fix global footer css --- .../global-footer.component.html | 10 ++++---- .../global-footer.component.scss | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 1765bc6fc..fbc2c89eb 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -27,27 +27,27 @@
-
+
@if (!env.customize?.theme) { - @if (!env.customize?.theme) { -
+
} @if (!enterpriseInfo?.footer_img) { - diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.scss b/frontend/src/app/shared/components/global-footer/global-footer.component.scss index b815da754..bf47d5489 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.scss +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.scss @@ -159,7 +159,7 @@ footer .nowrap { display: block; } -@media (min-width: 951px) { +@media (min-width: 1020px) { :host-context(.ltr-layout) .language-selector { float: right !important; } @@ -177,7 +177,24 @@ footer .nowrap { } .services { - @media (min-width: 951px) and (max-width: 1147px) { + @media (min-width: 1300px) { + :host-context(.ltr-layout) .language-selector { + float: right !important; + } + :host-context(.rtl-layout) .language-selector { + float: left !important; + } + + .explore-tagline-desktop { + display: block; + } + + .explore-tagline-mobile { + display: none; + } + } + + @media (max-width: 1300px) { :host-context(.ltr-layout) .services .language-selector { float: none !important; } @@ -253,7 +270,7 @@ footer .nowrap { } -@media (max-width: 950px) { +@media (max-width: 1019px) { .main-logo { width: 220px; @@ -292,7 +309,7 @@ footer .nowrap { } } -@media (max-width: 1147px) { +@media (max-width: 1300px) { .services.main-logo { width: 220px; From ae2ed8fdae41145d23af1f7ecd2330e40732dc15 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 20 Aug 2024 11:53:48 +0200 Subject: [PATCH 04/94] [mining] fix pools updater only running at start --- backend/src/config.ts | 2 ++ backend/src/index.ts | 1 + backend/src/tasks/pools-updater.ts | 9 +++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/config.ts b/backend/src/config.ts index b0afe7f23..cf63a0c7d 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -32,6 +32,7 @@ interface IConfig { AUTOMATIC_POOLS_UPDATE: boolean; POOLS_JSON_URL: string, POOLS_JSON_TREE_URL: string, + POOLS_UPDATE_DELAY: number, AUDIT: boolean; RUST_GBT: boolean; LIMIT_GBT: boolean; @@ -192,6 +193,7 @@ const defaults: IConfig = { 'AUTOMATIC_POOLS_UPDATE': false, 'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', 'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', + 'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week 'AUDIT': false, 'RUST_GBT': false, 'LIMIT_GBT': false, diff --git a/backend/src/index.ts b/backend/src/index.ts index 1d83c56a3..fa380b0c9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -239,6 +239,7 @@ class Server { if (config.FIAT_PRICE.ENABLED) { priceUpdater.$run(); } + await poolsUpdater.updatePoolsJson(); // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS const elapsed = Date.now() - start; diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index a3a3265c6..6b56fff95 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -23,11 +23,8 @@ class PoolsUpdater { return; } - const oneWeek = 604800; - const oneDay = 86400; - const now = new Date().getTime() / 1000; - if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart + if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart return; } @@ -87,8 +84,8 @@ class PoolsUpdater { logger.info(`Mining pools-v2.json (${githubSha}) import completed`); } catch (e) { - this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week - logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + this.lastRun = now - 600; // Try again in 10 minutes + logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, logger.tags.mining); } } From 459639410081554ddf7a5f523f2790a700f9e329 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 20 Aug 2024 12:07:20 +0200 Subject: [PATCH 05/94] [mining] pool updater is now self contained service --- backend/src/index.ts | 3 ++- backend/src/tasks/pools-updater.ts | 40 ++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index fa380b0c9..446a6a140 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -211,6 +211,8 @@ class Server { } }); } + + poolsUpdater.$startService(); } async runMainUpdateLoop(): Promise { @@ -239,7 +241,6 @@ class Server { if (config.FIAT_PRICE.ENABLED) { priceUpdater.$run(); } - await poolsUpdater.updatePoolsJson(); // rerun immediately if we skipped the mempool update, otherwise wait POLL_RATE_MS const elapsed = Date.now() - start; diff --git a/backend/src/tasks/pools-updater.ts b/backend/src/tasks/pools-updater.ts index 6b56fff95..652383a2a 100644 --- a/backend/src/tasks/pools-updater.ts +++ b/backend/src/tasks/pools-updater.ts @@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info'; import logger from '../logger'; import { SocksProxyAgent } from 'socks-proxy-agent'; import * as https from 'https'; +import { Common } from '../api/common'; /** * Maintain the most recent version of pools-v2.json */ class PoolsUpdater { + tag = 'PoolsUpdater'; + lastRun: number = 0; currentSha: string | null = null; poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL; treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL; + public async $startService(): Promise { + while ('Bitcoin is still alive') { + try { + await this.updatePoolsJson(); + } catch (e: any) { + logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag); + } + await Common.sleep$(10000); + } + } + public async updatePoolsJson(): Promise { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false || config.MEMPOOL.ENABLED === false @@ -40,7 +54,7 @@ class PoolsUpdater { this.currentSha = await this.getShaFromDb(); } - logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`); + logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag); if (this.currentSha !== null && this.currentSha === githubSha) { return; } @@ -50,16 +64,16 @@ class PoolsUpdater { config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled !process.env.npm_config_update_pools // We're not manually updating mining pool ) { - logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`); - logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`); + logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag); + logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag); return; } const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet'; if (this.currentSha === null) { - logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag); } else { - logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining); + logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag); } const poolsJson = await this.query(this.poolsUrl); if (poolsJson === undefined) { @@ -68,7 +82,7 @@ class PoolsUpdater { poolsParser.setMiningPools(poolsJson); if (config.DATABASE.ENABLED === false) { // Don't run db operations - logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag); return; } @@ -78,14 +92,14 @@ class PoolsUpdater { await this.updateDBSha(githubSha); await DB.query('COMMIT;'); } catch (e) { - logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag); await DB.query('ROLLBACK;'); } - logger.info(`Mining pools-v2.json (${githubSha}) import completed`); + logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag); } catch (e) { this.lastRun = now - 600; // Try again in 10 minutes - logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, logger.tags.mining); + logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag); } } @@ -99,7 +113,7 @@ class PoolsUpdater { await DB.query('DELETE FROM state where name="pools_json_sha"'); await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`); } catch (e) { - logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); } } } @@ -112,7 +126,7 @@ class PoolsUpdater { const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"'); return (rows.length > 0 ? rows[0].string : null); } catch (e) { - logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining); + logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag); return null; } } @@ -131,7 +145,7 @@ class PoolsUpdater { } } - logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining); + logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag); return null; } @@ -183,7 +197,7 @@ class PoolsUpdater { } return data.data; } catch (e) { - logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e)); + logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag); retry++; } await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); From 6db4afe878082c67e3bb7045cb78d4a9add567ef Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 20 Aug 2024 14:31:07 +0200 Subject: [PATCH 06/94] [mining] add POOLS_UPDATE_DELAY where needed --- backend/mempool-config.sample.json | 1 + backend/src/__fixtures__/mempool-config.template.json | 1 + backend/src/__tests__/config.test.ts | 1 + docker/README.md | 2 ++ docker/backend/mempool-config.json | 1 + docker/backend/start.sh | 2 ++ 6 files changed, 8 insertions(+) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 4650c1e64..8cd7f2ede 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -27,6 +27,7 @@ "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": false, "RUST_GBT": false, "LIMIT_GBT": false, diff --git a/backend/src/__fixtures__/mempool-config.template.json b/backend/src/__fixtures__/mempool-config.template.json index 3796b7f22..00049725a 100644 --- a/backend/src/__fixtures__/mempool-config.template.json +++ b/backend/src/__fixtures__/mempool-config.template.json @@ -28,6 +28,7 @@ "INDEXING_BLOCKS_AMOUNT": 14, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": 604800, "AUDIT": true, "RUST_GBT": false, "LIMIT_GBT": false, diff --git a/backend/src/__tests__/config.test.ts b/backend/src/__tests__/config.test.ts index 050213143..d112ab6eb 100644 --- a/backend/src/__tests__/config.test.ts +++ b/backend/src/__tests__/config.test.ts @@ -41,6 +41,7 @@ describe('Mempool Backend Config', () => { STDOUT_LOG_MIN_PRIORITY: 'debug', POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master', POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json', + POOLS_UPDATE_DELAY: 604800, AUDIT: false, RUST_GBT: false, LIMIT_GBT: false, diff --git a/docker/README.md b/docker/README.md index ce1548e91..2658914eb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over "AUTOMATIC_POOLS_UPDATE": false, "POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json", "POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master", + "POOLS_UPDATE_DELAY": 604800, "CPFP_INDEXING": false, "MAX_BLOCKS_BULK_QUERY": 0, "DISK_CACHE_BLOCK_INTERVAL": 6, @@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides: MEMPOOL_AUTOMATIC_POOLS_UPDATE: "" MEMPOOL_POOLS_JSON_URL: "" MEMPOOL_POOLS_JSON_TREE_URL: "" + MEMPOOL_POOLS_UPDATE_DELAY: "" MEMPOOL_CPFP_INDEXING: "" MEMPOOL_MAX_BLOCKS_BULK_QUERY: "" MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: "" diff --git a/docker/backend/mempool-config.json b/docker/backend/mempool-config.json index 79cd14644..7b00d792a 100644 --- a/docker/backend/mempool-config.json +++ b/docker/backend/mempool-config.json @@ -36,6 +36,7 @@ "ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__, "POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__", "POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__", + "POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__, "PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__, "MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__ }, diff --git a/docker/backend/start.sh b/docker/backend/start.sh index 8033531ef..9c98fed44 100755 --- a/docker/backend/start.sh +++ b/docker/backend/start.sh @@ -29,6 +29,7 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master} +__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800} __MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false} __MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false} __MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false} @@ -187,6 +188,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json +sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json From d22743c4b81331768d4e44cab5f4a0d213743434 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 22 Aug 2024 15:39:20 +0200 Subject: [PATCH 07/94] Don't display accelerator checkout on already accelerated txs --- .../app/components/transaction/transaction.component.html | 8 ++++---- .../app/components/transaction/transaction.component.ts | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 715fca4c8..553d3221f 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -552,18 +552,18 @@ @if (eta.blocks >= 7) { - + Not any time soon - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration && notAcceleratedOnLoad) { Accelerate } } @else if (network === 'liquid' || network === 'liquidtestnet') { } @else { - + - @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration) { + @if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && eligibleForAcceleration && notAcceleratedOnLoad) { Accelerate } diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..4d0818c72 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -139,6 +139,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { firstLoad = true; waitingForAccelerationInfo: boolean = false; isLoadingFirstSeen = false; + notAcceleratedOnLoad: boolean = null; featuresEnabled: boolean; segwitEnabled: boolean; @@ -848,6 +849,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.tx.feeDelta = cpfpInfo.feeDelta; this.setIsAccelerated(firstCpfp); } + + if (this.notAcceleratedOnLoad === null) { + this.notAcceleratedOnLoad = !this.isAcceleration; + } if (!this.isAcceleration && this.fragmentParams.has('accelerate')) { this.forceAccelerationSummary = true; @@ -1083,6 +1088,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { (!this.hideAccelerationSummary && !this.accelerationFlowCompleted) || this.forceAccelerationSummary ) + && this.notAcceleratedOnLoad // avoid briefly showing accelerator checkout on already accelerated txs ); } From b47e1486775a35593a8b9233cdee3c700fe463b3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 22 Aug 2024 19:51:28 +0000 Subject: [PATCH 08/94] respect json Accept header in API error responses --- backend/src/api/bitcoin/bitcoin.routes.ts | 144 +++++++++++--------- backend/src/api/explorer/channels.routes.ts | 19 +-- backend/src/api/explorer/general.routes.ts | 8 +- backend/src/api/explorer/nodes.routes.ts | 39 +++--- backend/src/api/liquid/liquid.routes.ts | 35 ++--- backend/src/api/mining/mining-routes.ts | 73 +++++----- backend/src/utils/api.ts | 9 ++ 7 files changed, 177 insertions(+), 150 deletions(-) create mode 100644 backend/src/utils/api.ts diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index 6225a9c1d..498003d98 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment'; import transactionRepository from '../../repositories/TransactionRepository'; import rbfCache from '../rbf-cache'; import { calculateMempoolTxCpfp } from '../cpfp'; +import { handleError } from '../../utils/api'; class BitcoinRoutes { public initRoutes(app: Application) { @@ -86,7 +87,7 @@ class BitcoinRoutes { res.set('Content-Type', 'application/json'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -105,13 +106,13 @@ class BitcoinRoutes { const result = mempoolBlocks.getMempoolBlocks(); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private getTransactionTimes(req: Request, res: Response) { if (!Array.isArray(req.query.txId)) { - res.status(500).send('Not an array'); + handleError(req, res, 500, 'Not an array'); return; } const txIds: string[] = []; @@ -128,12 +129,12 @@ class BitcoinRoutes { private async $getBatchedOutspends(req: Request, res: Response): Promise { const txids_csv = req.query.txids; if (!txids_csv || typeof txids_csv !== 'string') { - res.status(500).send('Invalid txids format'); + handleError(req, res, 500, 'Invalid txids format'); return; } const txids = txids_csv.split(','); if (txids.length > 50) { - res.status(400).send('Too many txids requested'); + handleError(req, res, 400, 'Too many txids requested'); return; } @@ -141,13 +142,13 @@ class BitcoinRoutes { const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); res.json(batchedOutspends); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getCpfpInfo(req: Request, res: Response) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { - res.status(501).send(`Invalid transaction ID.`); + handleError(req, res, 501, `Invalid transaction ID.`); return; } @@ -180,7 +181,7 @@ class BitcoinRoutes { try { cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); } catch (e) { - res.status(500).send('failed to get CPFP info'); + handleError(req, res, 500, 'failed to get CPFP info'); return; } } @@ -209,7 +210,7 @@ class BitcoinRoutes { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -223,7 +224,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -284,13 +285,13 @@ class BitcoinRoutes { // Not modified // 422 Unprocessable Entity // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 - res.status(422).send(`Psbt had no missing nonWitnessUtxos.`); + handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`); } } catch (e: any) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -304,7 +305,7 @@ class BitcoinRoutes { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { statusCode = 404; } - res.status(statusCode).send(e instanceof Error ? e.message : e); + handleError(req, res, statusCode, e instanceof Error ? e.message : e); } } @@ -314,7 +315,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(transactions); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -336,7 +337,7 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.json(block); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -346,7 +347,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(blockHeader); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -357,10 +358,11 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`audit not available`); + handleError(req, res, 404, `audit not available`); + return; } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +373,8 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.json(auditSummary); } else { - return res.status(404).send(`transaction audit not available`); + handleError(req, res, 404, `transaction audit not available`); + return; } } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); @@ -388,42 +391,49 @@ class BitcoinRoutes { return await this.getLegacyBlocks(req, res); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getBlocksByBulk(req: Request, res: Response) { try { if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented - return res.status(404).send(`This API is only available for Bitcoin networks`); + handleError(req, res, 404, `This API is only available for Bitcoin networks`); + return; } if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) { - return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`); + return; } if (!Common.indexingEnabled()) { - return res.status(404).send(`Indexing is required for this API`); + handleError(req, res, 404, `Indexing is required for this API`); + return; } const from = parseInt(req.params.from, 10); if (!req.params.from || from < 0) { - return res.status(400).send(`Parameter 'from' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`); + return; } const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10); if (to < 0) { - return res.status(400).send(`Parameter 'to' must be a block height (integer)`); + handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`); + return; } if (from > to) { - return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`); + handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`); + return; } if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) { - return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`); + return; } res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await blocks.$getBlocksBetweenHeight(from, to)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -458,10 +468,10 @@ class BitcoinRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(returnBlocks); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } - + private async getBlockTransactions(req: Request, res: Response) { try { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); @@ -483,7 +493,7 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -492,13 +502,13 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddress(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -507,15 +517,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -528,23 +539,23 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getAddressTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Address summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.'); return; } } private async getScriptHash(req: Request, res: Response) { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -555,15 +566,16 @@ class BitcoinRoutes { res.json(addressData); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - return res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); + return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactions(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND === 'none') { - res.status(405).send('Address lookups cannot be used with bitcoind as backend.'); + handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); return; } @@ -578,16 +590,16 @@ class BitcoinRoutes { res.json(transactions); } catch (e) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { - res.status(413).send(e instanceof Error ? e.message : e); + handleError(req, res, 413, e instanceof Error ? e.message : e); return; } - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async getScriptHashTransactionSummary(req: Request, res: Response): Promise { if (config.MEMPOOL.BACKEND !== 'esplora') { - res.status(405).send('Scripthash summary lookups require mempool/electrs backend.'); + handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.'); return; } } @@ -597,7 +609,7 @@ class BitcoinRoutes { const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); res.send(blockHash); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -624,7 +636,7 @@ class BitcoinRoutes { const rawMempool = await bitcoinApi.$getRawMempool(); res.send(rawMempool); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -632,12 +644,13 @@ class BitcoinRoutes { try { const result = blocks.getCurrentBlockHeight(); if (!result) { - return res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); + return; } res.setHeader('content-type', 'text/plain'); res.send(result.toString()); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -647,7 +660,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'text/plain'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -657,7 +670,7 @@ class BitcoinRoutes { res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -666,7 +679,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -675,7 +688,7 @@ class BitcoinRoutes { const result = await bitcoinClient.validateAddress(req.params.address); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -688,7 +701,7 @@ class BitcoinRoutes { replaces }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -697,7 +710,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(false); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -706,7 +719,7 @@ class BitcoinRoutes { const result = rbfCache.getRbfTrees(true); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -719,7 +732,7 @@ class BitcoinRoutes { res.status(204).send(); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -728,7 +741,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$getOutspends(req.params.txId); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -738,10 +751,10 @@ class BitcoinRoutes { if (da) { res.json(da); } else { - res.status(503).send(`Service Temporarily Unavailable`); + handleError(req, res, 503, `Service Temporarily Unavailable`); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -752,7 +765,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -764,7 +777,7 @@ class BitcoinRoutes { const txIdResult = await bitcoinClient.sendRawTransaction(txHex); res.send(txIdResult); } catch (e: any) { - res.status(400).send(e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } @@ -776,8 +789,7 @@ class BitcoinRoutes { const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { - res.setHeader('content-type', 'text/plain'); - res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } } diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 391bf628e..8b4c3e8c8 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -1,6 +1,7 @@ import config from '../../config'; import { Application, Request, Response } from 'express'; import channelsApi from './channels.api'; +import { handleError } from '../../utils/api'; class ChannelsRoutes { constructor() { } @@ -22,7 +23,7 @@ class ChannelsRoutes { const channels = await channelsApi.$searchChannelsById(req.params.search); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -38,7 +39,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channel); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -53,11 +54,11 @@ class ChannelsRoutes { const status: string = typeof req.query.status === 'string' ? req.query.status : ''; if (index < -1) { - res.status(400).send('Invalid index'); + handleError(req, res, 400, 'Invalid index'); return; } if (['open', 'active', 'closed'].includes(status) === false) { - res.status(400).send('Invalid status'); + handleError(req, res, 400, 'Invalid status'); return; } @@ -69,14 +70,14 @@ class ChannelsRoutes { res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } private async $getChannelsByTransactionIds(req: Request, res: Response): Promise { try { if (!Array.isArray(req.query.txId)) { - res.status(400).send('Not an array'); + handleError(req, res, 400, 'Not an array'); return; } const txIds: string[] = []; @@ -107,7 +108,7 @@ class ChannelsRoutes { res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -119,7 +120,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -132,7 +133,7 @@ class ChannelsRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(channels); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/explorer/general.routes.ts b/backend/src/api/explorer/general.routes.ts index 07620e84a..b4d0c635d 100644 --- a/backend/src/api/explorer/general.routes.ts +++ b/backend/src/api/explorer/general.routes.ts @@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import channelsApi from './channels.api'; import statisticsApi from './statistics.api'; +import { handleError } from '../../utils/api'; + class GeneralLightningRoutes { constructor() { } @@ -27,7 +29,7 @@ class GeneralLightningRoutes { channels: channels, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -41,7 +43,7 @@ class GeneralLightningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -50,7 +52,7 @@ class GeneralLightningRoutes { const statistics = await statisticsApi.$getLatestStatistics(); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 9d6373845..9ca2fd1c3 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import DB from '../../database'; import { INodesRanking } from '../../mempool.interfaces'; +import { handleError } from '../../utils/api'; class NodesRoutes { constructor() { } @@ -31,7 +32,7 @@ class NodesRoutes { const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -181,13 +182,13 @@ class NodesRoutes { } } catch (e) {} } - + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(nodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -195,7 +196,7 @@ class NodesRoutes { try { const node = await nodesApi.$getNode(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -203,7 +204,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -215,7 +216,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -223,7 +224,7 @@ class NodesRoutes { try { const node = await nodesApi.$getFeeHistogram(req.params.public_key); if (!node) { - res.status(404).send('Node not found'); + handleError(req, res, 404, 'Node not found'); return; } res.header('Pragma', 'public'); @@ -231,7 +232,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -247,7 +248,7 @@ class NodesRoutes { topByChannels: topChannelsNodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -259,7 +260,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -271,7 +272,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -283,7 +284,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(topCapacityNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -295,7 +296,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -307,7 +308,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(worldNodes); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -322,7 +323,7 @@ class NodesRoutes { ); if (country.length === 0) { - res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`); return; } @@ -335,7 +336,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -349,7 +350,7 @@ class NodesRoutes { ); if (isp.length === 0) { - res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`); + handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`); return; } @@ -362,7 +363,7 @@ class NodesRoutes { nodes: nodes, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -374,7 +375,7 @@ class NodesRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.json(nodesPerAs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/api/liquid/liquid.routes.ts b/backend/src/api/liquid/liquid.routes.ts index 9ea61ca31..9dafd0def 100644 --- a/backend/src/api/liquid/liquid.routes.ts +++ b/backend/src/api/liquid/liquid.routes.ts @@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express'; import config from '../../config'; import elementsParser from './elements-parser'; import icons from './icons'; +import { handleError } from '../../utils/api'; class LiquidRoutes { public initRoutes(app: Application) { @@ -42,7 +43,7 @@ class LiquidRoutes { res.setHeader('content-length', result.length); res.send(result); } else { - res.status(404).send('Asset icon not found'); + handleError(req, res, 404, 'Asset icon not found'); } } @@ -51,7 +52,7 @@ class LiquidRoutes { if (result) { res.json(result); } else { - res.status(404).send('Asset icons not found'); + handleError(req, res, 404, 'Asset icons not found'); } } @@ -82,7 +83,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(pegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -94,7 +95,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.json(reserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -106,7 +107,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentSupply); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -118,7 +119,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(currentReserves); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -130,7 +131,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(auditStatus); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -142,7 +143,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -154,7 +155,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationAddresses); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -166,7 +167,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -178,7 +179,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(expiredUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -190,7 +191,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(federationUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -202,7 +203,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -214,7 +215,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(emergencySpentUtxos); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -226,7 +227,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(recentPegs); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -238,7 +239,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsVolume); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -250,7 +251,7 @@ class LiquidRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.json(pegsCount); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 8f8bbac82..69e6d95d4 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -10,6 +10,7 @@ import mining from "./mining"; import PricesRepository from '../../repositories/PricesRepository'; import AccelerationRepository from '../../repositories/AccelerationRepository'; import accelerationApi from '../services/acceleration'; +import { handleError } from '../../utils/api'; class MiningRoutes { public initRoutes(app: Application) { @@ -53,12 +54,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Prices are not available on testnets.'); + handleError(req, res, 400, 'Prices are not available on testnets.'); return; } const timestamp = parseInt(req.query.timestamp as string, 10) || 0; const currency = req.query.currency as string; - + let response; if (timestamp && currency) { response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency); @@ -71,7 +72,7 @@ class MiningRoutes { } res.status(200).send(response); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -84,9 +85,9 @@ class MiningRoutes { res.json(stats); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -103,9 +104,9 @@ class MiningRoutes { res.json(poolBlocks); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -129,7 +130,7 @@ class MiningRoutes { res.json(pools); } } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -143,7 +144,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(stats); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -157,7 +158,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(hashrates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -172,9 +173,9 @@ class MiningRoutes { res.json(hashrates); } catch (e) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { - res.status(404).send(e.message); + handleError(req, res, 404, e.message); } else { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } @@ -203,7 +204,7 @@ class MiningRoutes { currentDifficulty: currentDifficulty, }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -217,7 +218,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -235,7 +236,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFees); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -249,7 +250,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockRewards); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -263,7 +264,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blockFeeRates); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -281,7 +282,7 @@ class MiningRoutes { weights: blockWeights }); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -293,7 +294,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -317,7 +318,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -326,7 +327,7 @@ class MiningRoutes { const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash); if (!audit) { - res.status(204).send(`This block has not been audited.`); + handleError(req, res, 204, `This block has not been audited.`); return; } @@ -335,7 +336,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -358,7 +359,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.json(result); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -371,7 +372,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -384,7 +385,7 @@ class MiningRoutes { res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.json(audit || 'null'); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -394,12 +395,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -409,13 +410,13 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -425,12 +426,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -440,12 +441,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(await AccelerationRepository.$getAccelerationTotals(req.query.pool, req.query.interval)); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -455,12 +456,12 @@ class MiningRoutes { res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) { - res.status(400).send('Acceleration data is not available.'); + handleError(req, res, 400, 'Acceleration data is not available.'); return; } res.status(200).send(accelerationApi.accelerations || []); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } @@ -472,7 +473,7 @@ class MiningRoutes { accelerationApi.accelerationRequested(req.params.txid); res.status(200).send(); } catch (e) { - res.status(500).send(e instanceof Error ? e.message : e); + handleError(req, res, 500, e instanceof Error ? e.message : e); } } } diff --git a/backend/src/utils/api.ts b/backend/src/utils/api.ts new file mode 100644 index 000000000..69d746b9f --- /dev/null +++ b/backend/src/utils/api.ts @@ -0,0 +1,9 @@ +import { Request, Response } from 'express'; + +export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void { + if (req.accepts('json')) { + res.status(statusCode).json({ error: errorMessage }); + } else { + res.status(statusCode).send(errorMessage); + } +} \ No newline at end of file From 185eae00e9af812032b37852d4551c2b4ef6a04a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 25 Aug 2024 22:38:00 +0000 Subject: [PATCH 09/94] Fix RBF tracking inconsistencies --- backend/src/api/rbf-cache.ts | 121 ++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 39 deletions(-) diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index a087abbe0..f4b192d3a 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -44,6 +44,22 @@ interface CacheEvent { value?: any, } +/** + * Singleton for tracking RBF trees + * + * Maintains a set of RBF trees, where each tree represents a sequence of + * consecutive RBF replacements. + * + * Trees are identified by the txid of the root transaction. + * + * To maintain consistency, the following invariants must be upheld: + * - Symmetry: replacedBy(A) = B <=> A in replaces(B) + * - Unique id: treeMap(treeMap(X)) = treeMap(X) + * - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B) + * - Existence: X in treeMap => treeMap(X) in rbfTrees + * - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap + */ + class RbfCache { private replacedBy: Map = new Map(); private replaces: Map = new Map(); @@ -61,6 +77,10 @@ class RbfCache { setInterval(this.cleanup.bind(this), 1000 * 60 * 10); } + /** + * Low level cache operations + */ + private addTx(txid: string, tx: MempoolTransactionExtended): void { this.txs.set(txid, tx); this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid }); @@ -92,6 +112,12 @@ class RbfCache { this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid }); } + /** + * Basic data structure operations + * must uphold tree invariants + */ + + public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void { if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) { return; @@ -114,6 +140,10 @@ class RbfCache { if (!replacedTx.rbf) { txFullRbf = true; } + if (this.replacedBy.has(replacedTx.txid)) { + // should never happen + continue; + } this.replacedBy.set(replacedTx.txid, newTx.txid); if (this.treeMap.has(replacedTx.txid)) { const treeId = this.treeMap.get(replacedTx.txid); @@ -140,18 +170,47 @@ class RbfCache { } } newTx.fullRbf = txFullRbf; - const treeId = replacedTrees[0].tx.txid; const newTree = { tx: newTx, time: newTime, fullRbf: treeFullRbf, replaces: replacedTrees }; - this.addTree(treeId, newTree); - this.updateTreeMap(treeId, newTree); + this.addTree(newTree.tx.txid, newTree); + this.updateTreeMap(newTree.tx.txid, newTree); this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid)); } + public mined(txid): void { + if (!this.txs.has(txid)) { + return; + } + const treeId = this.treeMap.get(txid); + if (treeId && this.rbfTrees.has(treeId)) { + const tree = this.rbfTrees.get(treeId); + if (tree) { + this.setTreeMined(tree, txid); + tree.mined = true; + this.dirtyTrees.add(treeId); + this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); + } + } + this.evict(txid); + } + + // flag a transaction as removed from the mempool + public evict(txid: string, fast: boolean = false): void { + this.evictionCount++; + if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { + const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours + this.addExpiration(txid, expiryTime); + } + } + + /** + * Read-only public interface + */ + public has(txId: string): boolean { return this.txs.has(txId); } @@ -232,32 +291,6 @@ class RbfCache { return changes; } - public mined(txid): void { - if (!this.txs.has(txid)) { - return; - } - const treeId = this.treeMap.get(txid); - if (treeId && this.rbfTrees.has(treeId)) { - const tree = this.rbfTrees.get(treeId); - if (tree) { - this.setTreeMined(tree, txid); - tree.mined = true; - this.dirtyTrees.add(treeId); - this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId }); - } - } - this.evict(txid); - } - - // flag a transaction as removed from the mempool - public evict(txid: string, fast: boolean = false): void { - this.evictionCount++; - if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) { - const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours - this.addExpiration(txid, expiryTime); - } - } - // is the transaction involved in a full rbf replacement? public isFullRbf(txid: string): boolean { const treeId = this.treeMap.get(txid); @@ -271,6 +304,10 @@ class RbfCache { return tree?.fullRbf; } + /** + * Cache maintenance & utility functions + */ + private cleanup(): void { const now = Date.now(); for (const txid of this.expiring.keys()) { @@ -299,10 +336,6 @@ class RbfCache { for (const tx of (replaces || [])) { // recursively remove prior versions from the cache this.replacedBy.delete(tx); - // if this is the id of a tree, remove that too - if (this.treeMap.get(tx) === tx) { - this.removeTree(tx); - } this.remove(tx); } } @@ -376,8 +409,15 @@ class RbfCache { this.txs.set(txEntry.value.txid, txEntry.value); }); this.staleCount = 0; - for (const deflatedTree of trees) { - await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) { + const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs); + if (tree) { + this.addTree(tree.tx.txid, tree); + this.updateTreeMap(tree.tx.txid, tree); + if (tree.mined) { + this.evict(tree.tx.txid); + } + } } expiring.forEach(expiringEntry => { if (this.txs.has(expiringEntry.key)) { @@ -426,6 +466,12 @@ class RbfCache { return; } + // if this tx is already in the cache, return early + if (this.treeMap.has(txid)) { + this.removeTree(deflated.key); + return; + } + // recursively reconstruct child trees for (const childId of treeInfo.replaces) { const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined); @@ -457,10 +503,6 @@ class RbfCache { fullRbf: treeInfo.fullRbf, replaces, }; - this.treeMap.set(txid, root); - if (root === txid) { - this.addTree(root, tree); - } return tree; } @@ -511,6 +553,7 @@ class RbfCache { processTxs(txs); } + // evict missing transactions for (const txid of txids) { if (!found[txid]) { this.evict(txid, false); From e362003746e237adbfc2681695b956f05ece80ef Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 26 Aug 2024 21:51:49 +0000 Subject: [PATCH 10/94] Catch RBF replacements across mempool update boundaries --- backend/src/api/common.ts | 16 +++++++------- backend/src/api/mempool.ts | 31 ++++++++++++---------------- backend/src/api/websocket-handler.ts | 19 +++++++++++++---- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 13fc86147..f3d3e43b5 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -80,8 +80,8 @@ export class Common { return arr; } - static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } { - const matches: { [txid: string]: MempoolTransactionExtended[] } = {}; + static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} { + const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {}; // For small N, a naive nested loop is extremely fast, but it doesn't scale if (added.length < 1000 && deleted.length < 50 && !forceScalable) { @@ -96,7 +96,7 @@ export class Common { addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout)); }); if (foundMatches?.length) { - matches[addedTx.txid] = [...new Set(foundMatches)]; + matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx }; } }); } else { @@ -124,7 +124,7 @@ export class Common { foundMatches.add(deletedTx); } if (foundMatches.size) { - matches[addedTx.txid] = [...foundMatches]; + matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx }; } } } @@ -139,17 +139,17 @@ export class Common { const replaced: Set = new Set(); for (let i = 0; i < tx.vin.length; i++) { const vin = tx.vin[i]; - const match = spendMap.get(`${vin.txid}:${vin.vout}`); + const key = `${vin.txid}:${vin.vout}`; + const match = spendMap.get(key); if (match && match.txid !== tx.txid) { replaced.add(match); // remove this tx from the spendMap // prevents the same tx being replaced more than once for (const replacedVin of match.vin) { - const key = `${replacedVin.txid}:${replacedVin.vout}`; - spendMap.delete(key); + const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`; + spendMap.delete(replacedKey); } } - const key = `${vin.txid}:${vin.vout}`; spendMap.delete(key); } if (replaced.size) { diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 1f55179fb..1442b05fa 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -19,12 +19,13 @@ class Mempool { private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCandidates: { [txid: string ]: boolean } = {}; private spendMap = new Map(); + private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 }; private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined; private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], - deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; + deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise) | undefined; private accelerations: { [txId: string]: Acceleration } = {}; private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {}; @@ -74,12 +75,12 @@ class Mempool { } public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void { + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void { this.mempoolChangedCallback = fn; } public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise): void { this.$asyncMempoolChangedCallback = fn; } @@ -362,12 +363,15 @@ class Mempool { const candidatesChanged = candidates?.added?.length || candidates?.removed?.length; - if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { - this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta); + this.recentlyDeleted.unshift(deletedTransactions); + this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates + + if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) { + this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta); } - if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) { + if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) { this.updateTimerProgress(timer, 'running async mempool callback'); - await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates); + await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates); this.updateTimerProgress(timer, 'completed async mempool callback'); } @@ -541,16 +545,7 @@ class Mempool { } } - public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void { - for (const rbfTransaction in rbfTransactions) { - if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) { - // Store replaced transactions - rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]); - } - } - } - - public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { + public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void { for (const rbfTransaction in rbfTransactions) { if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) { // Store replaced transactions diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 79a783f88..2a047472e 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -520,8 +520,17 @@ class WebsocketHandler { } } + /** + * + * @param newMempool + * @param mempoolSize + * @param newTransactions array of transactions added this mempool update. + * @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first. + * @param accelerationDelta + * @param candidates + */ async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, - newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], + newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates): Promise { if (!this.webSocketServers.length) { throw new Error('No WebSocket.Server have been set'); @@ -529,6 +538,8 @@ class WebsocketHandler { this.printLogs(); + const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : []; + const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool); let added = newTransactions; let removed = deletedTransactions; @@ -547,7 +558,7 @@ class WebsocketHandler { const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); const mempoolInfo = memPool.getMempoolInfo(); const vBytesPerSecond = memPool.getVBytesPerSecond(); - const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); + const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const da = difficultyAdjustment.getDifficultyAdjustment(); const accelerations = memPool.getAccelerations(); memPool.handleRbfTransactions(rbfTransactions); @@ -578,7 +589,7 @@ class WebsocketHandler { const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const tx of newTransactions) { if (rbfTransactions[tx.txid]) { - for (const replaced of rbfTransactions[tx.txid]) { + for (const replaced of rbfTransactions[tx.txid].replaced) { replacedTransactions.push({ replaced: replaced.txid, by: tx }); } } @@ -947,7 +958,7 @@ class WebsocketHandler { await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions)); const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap()); - memPool.handleMinedRbfTransactions(rbfTransactions); + memPool.handleRbfTransactions(rbfTransactions); memPool.removeFromSpendMap(transactions); if (config.MEMPOOL.AUDIT && memPool.isInSync()) { From ee53597fe9805ce8c5de3b6e17deac7b3159cb30 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 26 Aug 2024 23:22:39 +0000 Subject: [PATCH 11/94] Resume RBF trees after restart --- backend/src/api/disk-cache.ts | 1 + backend/src/api/rbf-cache.ts | 27 ++++++++++++++++++++++++++- backend/src/api/redis-cache.ts | 1 + 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/src/api/disk-cache.ts b/backend/src/api/disk-cache.ts index 202f8f4cb..f2a1f2390 100644 --- a/backend/src/api/disk-cache.ts +++ b/backend/src/api/disk-cache.ts @@ -257,6 +257,7 @@ class DiskCache { trees: rbfData.rbf.trees, expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })), mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } } catch (e) { diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index f4b192d3a..944ad790e 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -403,7 +403,7 @@ class RbfCache { }; } - public async load({ txs, trees, expiring, mempool }): Promise { + public async load({ txs, trees, expiring, mempool, spendMap }): Promise { try { txs.forEach(txEntry => { this.txs.set(txEntry.value.txid, txEntry.value); @@ -425,6 +425,31 @@ class RbfCache { } }); this.staleCount = 0; + + // connect cached trees to current mempool transactions + const conflicts: Record }> = {}; + for (const tree of this.rbfTrees.values()) { + const tx = this.getTx(tree.tx.txid); + if (!tx || tree.mined) { + continue; + } + for (const vin of tx.vin) { + const conflict = spendMap.get(`${vin.txid}:${vin.vout}`); + if (conflict && conflict.txid !== tx.txid) { + if (!conflicts[conflict.txid]) { + conflicts[conflict.txid] = { + replacedBy: conflict, + replaces: new Set(), + }; + } + conflicts[conflict.txid].replaces.add(tx); + } + } + } + for (const { replacedBy, replaces } of Object.values(conflicts)) { + this.add([...replaces.values()], replacedBy); + } + await this.checkTrees(); logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`); this.cleanup(); diff --git a/backend/src/api/redis-cache.ts b/backend/src/api/redis-cache.ts index cbfa2f18b..1caade15b 100644 --- a/backend/src/api/redis-cache.ts +++ b/backend/src/api/redis-cache.ts @@ -365,6 +365,7 @@ class RedisCache { trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }), expiring: rbfExpirations, mempool: memPool.getMempool(), + spendMap: memPool.getSpendMap(), }); } From 9e05060af4de8411536fccc41098c69ae3775155 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 27 Aug 2024 00:17:17 +0000 Subject: [PATCH 12/94] fix tests --- backend/src/__tests__/api/common.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/__tests__/api/common.ts b/backend/src/__tests__/api/common.ts index 74a7db88f..14ae3c78b 100644 --- a/backend/src/__tests__/api/common.ts +++ b/backend/src/__tests__/api/common.ts @@ -1,5 +1,5 @@ import { Common } from '../../api/common'; -import { MempoolTransactionExtended } from '../../mempool.interfaces'; +import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces'; const randomTransactions = require('./test-data/transactions-random.json'); const replacedTransactions = require('./test-data/transactions-replaced.json'); @@ -10,14 +10,14 @@ describe('Common', () => { describe('RBF', () => { const newTransactions = rbfTransactions.concat(randomTransactions); test('should detect RBF transactions with fast method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); }); test('should detect RBF transactions with scalable method', () => { - const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true); + const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true); expect(Object.values(result).length).toEqual(2); expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6'); expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875'); From a3e61525fe76af94494be02f6a4c24e565aac2eb Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 27 Aug 2024 11:42:13 +0200 Subject: [PATCH 13/94] Reset acceleration flow state when leaving transaction --- frontend/src/app/components/transaction/transaction.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..6ff85c5bd 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -966,6 +966,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.filters = []; this.showCpfpDetails = false; this.showAccelerationDetails = false; + this.accelerationFlowCompleted = false; this.accelerationInfo = null; this.cashappEligible = false; this.txInBlockIndex = null; From 624b3473fc39ba34a1f13f82579379ee39e553d7 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 27 Aug 2024 11:29:29 +0200 Subject: [PATCH 14/94] Hide accelerator panel if tx gets accelerated on another session --- .../accelerate-checkout/accelerate-checkout.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6b1eadf7d..0bb37f15e 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -196,9 +196,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (changes.scrollEvent && this.scrollEvent) { this.scrollToElement('acceleratePreviewAnchor', 'start'); } - if (changes.accelerating) { - if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) { + if (changes.accelerating && this.accelerating) { + if (this.step === 'processing' || this.step === 'paid') { this.moveToStep('success'); + } else { // Edge case where the transaction gets accelerated by someone else or on another session + this.closeModal(); } } } From 555425d97eba920c39091c49e90f5223c8d9b20a Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 27 Aug 2024 14:49:54 +0200 Subject: [PATCH 15/94] Handle city-states in geolocation component --- .../shared/components/geolocation/geolocation.component.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.ts b/frontend/src/app/shared/components/geolocation/geolocation.component.ts index 1a498a1b2..85e4b6e53 100644 --- a/frontend/src/app/shared/components/geolocation/geolocation.component.ts +++ b/frontend/src/app/shared/components/geolocation/geolocation.component.ts @@ -70,6 +70,12 @@ export class GeolocationComponent implements OnChanges { if (this.type === 'node') { const city = this.data.city ? this.data.city : ''; + // Handle city-states like Singapore or Hong Kong + if (city && city === this.data?.country) { + this.formattedLocation = `${this.data.country} ${getFlagEmoji(this.data.iso)}`; + return; + } + // City this.formattedLocation = `${city}`; From 98d98b2478320d1e868a1f5df7cf4243b7a675b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 05:01:52 +0000 Subject: [PATCH 16/94] Bump micromatch from 4.0.4 to 4.0.8 in /frontend Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.4 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/4.0.8/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.4...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c17e706af..16400db7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,7 +32,6 @@ "bootstrap": "~4.6.2", "browserify": "^17.0.0", "clipboard": "^2.0.11", - "cypress": "^13.14.0", "domino": "^2.1.6", "echarts": "~5.5.0", "esbuild": "^0.23.0", @@ -12694,12 +12693,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" @@ -27622,12 +27621,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, "miller-rabin": { From b526ee0877f3a9c9ff4fef9ed6414221e424185c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 28 Aug 2024 14:38:12 +0000 Subject: [PATCH 17/94] Handle paginated acceleration results --- .../block/block-preview.component.ts | 2 +- .../app/components/block/block.component.ts | 2 +- .../components/tracker/tracker.component.ts | 2 +- .../transaction/transaction.component.ts | 2 +- .../src/app/services/services-api.service.ts | 25 ++++++++++++++++++- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts index 72da96818..572f91a38 100644 --- a/frontend/src/app/components/block/block-preview.component.ts +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -137,7 +137,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy { }) ), this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 5cba85e90..9da74cb62 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -319,7 +319,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.accelerationsSubscription = this.block$.pipe( switchMap((block) => { return this.stateService.env.ACCELERATOR === true && block.height > 819500 - ? this.servicesApiService.getAccelerationHistory$({ blockHeight: block.height }) + ? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height }) .pipe(catchError(() => { return of([]); })) diff --git a/frontend/src/app/components/tracker/tracker.component.ts b/frontend/src/app/components/tracker/tracker.component.ts index 24b5fc1dc..42156d2a9 100644 --- a/frontend/src/app/components/tracker/tracker.component.ts +++ b/frontend/src/app/components/tracker/tracker.component.ts @@ -286,7 +286,7 @@ export class TrackerComponent implements OnInit, OnDestroy { this.accelerationInfo = null; }), switchMap((blockHash: string) => { - return this.servicesApiService.getAccelerationHistory$({ blockHash }); + return this.servicesApiService.getAllAccelerationHistory$({ blockHash }, null, this.txId); }), catchError(() => { return of(null); diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 8c0d3b4a9..09e0d2874 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -343,7 +343,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.setIsAccelerated(); }), switchMap((blockHeight: number) => { - return this.servicesApiService.getAccelerationHistory$({ blockHeight }).pipe( + return this.servicesApiService.getAllAccelerationHistory$({ blockHeight }, null, this.txId).pipe( switchMap((accelerationHistory: Acceleration[]) => { if (this.tx.acceleration && !accelerationHistory.length) { // If the just mined transaction was accelerated, but services backend did not return any acceleration data, retry return throwError('retry'); diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 1366342f7..5213e131c 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { StateService } from './state.service'; import { StorageService } from './storage.service'; import { MenuGroup } from '../interfaces/services.interface'; -import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; +import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap, map } from 'rxjs'; import { IBackendInfo } from '../interfaces/websocket.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; @@ -160,6 +160,29 @@ export class ServicesApiServices { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params } }); } + getAllAccelerationHistory$(params: AccelerationHistoryParams, limit?: number, findTxid?: string): Observable { + const getPage$ = (page: number, accelerations: Acceleration[] = []): Observable<{ page: number, total: number, accelerations: Acceleration[] }> => { + return this.getAccelerationHistoryObserveResponse$({...params, page}).pipe( + map((response) => ({ + page, + total: parseInt(response.headers.get('X-Total-Count'), 10), + accelerations: accelerations.concat(response.body || []), + })), + switchMap(({page, total, accelerations}) => { + if (accelerations.length >= Math.min(total, limit ?? Infinity) || (findTxid && accelerations.find((acc) => acc.txid === findTxid))) { + return of({ page, total, accelerations }); + } else { + return getPage$(page + 1, accelerations); + } + }), + ); + }; + + return getPage$(1).pipe( + map(({ accelerations }) => accelerations), + ); + } + getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable { return this.httpClient.get(`${this.stateService.env.SERVICES_API}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'}); } From 0a5a2c3c7e40c46c2d2ddead1144c48d7c718038 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 28 Aug 2024 16:50:00 +0200 Subject: [PATCH 18/94] Remove difficulty epoch block offset --- .../difficulty-mining/difficulty-mining.component.ts | 2 +- .../src/app/components/difficulty/difficulty.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts index 90b41d749..e19f510b5 100644 --- a/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts +++ b/frontend/src/app/components/difficulty-mining/difficulty-mining.component.ts @@ -77,7 +77,7 @@ export class DifficultyMiningComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - remainingBlocks: da.remainingBlocks - 1, + remainingBlocks: da.remainingBlocks, colorAdjustments, colorPreviousAdjustments, newDifficultyHeight: da.nextRetargetHeight, diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index 579b49fc3..6a99aecef 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -153,8 +153,8 @@ export class DifficultyComponent implements OnInit { base: `${da.progressPercent.toFixed(2)}%`, change: da.difficultyChange, progress: da.progressPercent, - minedBlocks: this.currentIndex + 1, - remainingBlocks: da.remainingBlocks - 1, + minedBlocks: this.currentIndex, + remainingBlocks: da.remainingBlocks, expectedBlocks: Math.floor(da.expectedBlocks), colorAdjustments, colorPreviousAdjustments, From 0f1def58226038d8047fac2eb3e9fa530700df0a Mon Sep 17 00:00:00 2001 From: nymkappa <9780671+nymkappa@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:53:40 +0200 Subject: [PATCH 19/94] [accelerator] make bid boost graph bar min height taller --- .../acceleration-fees-graph.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts index d78b663a4..68a2bdd52 100644 --- a/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts +++ b/frontend/src/app/components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component.ts @@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest type: 'bar', barWidth: '90%', large: true, - barMinHeight: 1, + barMinHeight: 3, }, ], dataZoom: (this.widget || data.length === 0 )? undefined : [{ From 3e78b636d6935cba639bf1694c8dc0e47f0768c9 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Thu, 12 Sep 2024 16:02:11 +0200 Subject: [PATCH 20/94] [accelerator] avoid duplicated accel request with double click --- .../accelerate-checkout.component.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 6b1eadf7d..5c150212d 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -75,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { @Output() changeMode = new EventEmitter(); calculating = true; + processing = false; selectedOption: 'wait' | 'accel'; cantPayReason = ''; quoteError = ''; // error fetching estimate or initial data @@ -378,9 +379,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * Account-based acceleration request */ accelerateWithMempoolAccount(): void { - if (!this.canPay || this.calculating) { + if (!this.canPay || this.calculating || this.processing) { return; } + this.processing = true; if (this.accelerationSubscription) { this.accelerationSubscription.unsubscribe(); } @@ -390,6 +392,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); this.showSuccess = true; @@ -397,6 +400,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.moveToStep('paid'); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; } }); @@ -466,10 +470,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * APPLE PAY */ async requestApplePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -494,6 +502,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { console.error(`Unable to find apple pay button id='apple-pay-button'`); // Try again setTimeout(this.requestApplePayPayment.bind(this), 500); + this.processing = false; return; } this.loadingApplePay = false; @@ -505,6 +514,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -516,6 +526,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.applePay) { @@ -526,6 +537,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -537,6 +549,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -547,6 +560,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } catch (e) { + this.processing = false; console.error(e); } } @@ -557,10 +571,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * GOOGLE PAY */ async requestGooglePayPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } - + + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -595,6 +613,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) { console.error(`Cannot retreive payment card details`); this.accelerateError = 'apple_pay_no_card_details'; + this.processing = false; return; } const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); @@ -606,6 +625,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.googlePay) { @@ -616,6 +636,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { @@ -627,6 +648,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { } }); } else { + this.processing = false; let errorMessage = `Tokenization failed with status: ${tokenResult.status}`; if (tokenResult.errors) { errorMessage += ` and errors: ${JSON.stringify( @@ -644,10 +666,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy { * CASHAPP */ async requestCashAppPayment(): Promise { + if (this.processing) { + return; + } if (this.conversionsSubscription) { this.conversionsSubscription.unsubscribe(); } + this.processing = true; this.conversionsSubscription = this.stateService.conversions$.subscribe( async (conversions) => { this.conversions = conversions; @@ -678,6 +704,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.cashAppPay.addEventListener('ontokenization', event => { const { tokenResult, error } = event.detail; if (error) { + this.processing = false; this.accelerateError = error; } else if (tokenResult.status === 'OK') { this.servicesApiService.accelerateWithCashApp$( @@ -688,6 +715,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { this.accelerationUUID ).subscribe({ next: () => { + this.processing = false; this.apiService.logAccelerationRequest$(this.tx.txid).subscribe(); this.audioService.playSound('ascend-chime-cartoon'); if (this.cashAppPay) { @@ -702,6 +730,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy { }, 1000); }, error: (response) => { + this.processing = false; this.accelerateError = response.error; if (!(response.status === 403 && response.error === 'not_available')) { setTimeout(() => { From 4ccd3c8525b69a406f5b81293101185989a67d34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:10:33 +0000 Subject: [PATCH 21/94] Bump serve-static and express in /backend Bumps [serve-static](https://github.com/expressjs/serve-static) to 1.16.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `serve-static` from 1.15.0 to 1.16.2 - [Release notes](https://github.com/expressjs/serve-static/releases) - [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md) - [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2) Updates `express` from 4.19.2 to 4.21.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: serve-static dependency-type: indirect - dependency-name: express dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- backend/package-lock.json | 193 +++++++++++++++++++++----------------- backend/package.json | 2 +- 2 files changed, 107 insertions(+), 88 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 07cc9ffb3..7696eddd6 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,7 +16,7 @@ "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.0", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "redis": "^4.7.0", @@ -2490,9 +2490,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2502,7 +2502,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -3031,9 +3031,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3461,36 +3461,36 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -3603,12 +3603,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -6052,9 +6052,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6268,9 +6271,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6438,9 +6444,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6648,11 +6654,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -6873,9 +6879,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -6908,6 +6914,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6919,14 +6933,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -9605,9 +9619,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -9617,7 +9631,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -9998,9 +10012,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "error-ex": { "version": "1.3.2", @@ -10305,36 +10319,36 @@ } }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -10436,12 +10450,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -12238,9 +12252,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -12403,9 +12417,9 @@ } }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "on-finished": { "version": "2.4.1", @@ -12522,9 +12536,9 @@ "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -12666,11 +12680,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -12804,9 +12818,9 @@ "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -12838,6 +12852,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -12851,14 +12870,14 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { diff --git a/backend/package.json b/backend/package.json index 558a1d0b8..c18974021 100644 --- a/backend/package.json +++ b/backend/package.json @@ -45,7 +45,7 @@ "axios": "1.7.2", "bitcoinjs-lib": "~6.1.3", "crypto-js": "~4.2.0", - "express": "~4.19.2", + "express": "~4.21.0", "maxmind": "~4.3.11", "mysql2": "~3.11.0", "rust-gbt": "file:./rust-gbt", From 67eb815992f3b417592d9a4530ec6bd29178fc1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:10:42 +0000 Subject: [PATCH 22/94] Bump body-parser and express in /frontend Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `body-parser` from 1.20.2 to 1.20.3 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3) Updates `express` from 4.19.2 to 4.21.0 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 252 ++++++++++++++++++++++--------------- 1 file changed, 152 insertions(+), 100 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16400db7c..b53f80c88 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6019,9 +6019,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -6031,7 +6031,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -6066,11 +6066,11 @@ } }, "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -9875,36 +9875,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9923,6 +9923,14 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9940,11 +9948,11 @@ } }, "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -10177,12 +10185,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -10201,6 +10209,14 @@ "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -12667,9 +12683,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -13674,9 +13693,12 @@ } }, "node_modules/object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14190,9 +14212,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -15477,9 +15499,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -15618,19 +15640,27 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/server-destroy": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", @@ -15722,13 +15752,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22582,9 +22616,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -22594,7 +22628,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -22622,11 +22656,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } } } @@ -25550,36 +25584,36 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -25595,6 +25629,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -25609,11 +25648,11 @@ } }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "safe-buffer": { @@ -25788,12 +25827,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -25809,6 +25848,11 @@ "ms": "2.0.0" } }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -27601,9 +27645,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -28374,9 +28418,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "object-keys": { "version": "1.1.1", @@ -28750,9 +28794,9 @@ } }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "path-type": { "version": "4.0.0", @@ -29673,9 +29717,9 @@ } }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -29796,14 +29840,21 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } } }, "server-destroy": { @@ -29879,13 +29930,14 @@ "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { From c7ab6b03fb8fcdf3c37f2025c032d3a96c2e7ccc Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 13 Sep 2024 23:23:22 +0800 Subject: [PATCH 23/94] Fix critical calculator inputmode --- .../src/app/components/calculator/calculator.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/calculator/calculator.component.html b/frontend/src/app/components/calculator/calculator.component.html index e4ade67d2..e205479ee 100644 --- a/frontend/src/app/components/calculator/calculator.component.html +++ b/frontend/src/app/components/calculator/calculator.component.html @@ -12,7 +12,7 @@
{{ currency$ | async }}
- +
@@ -20,7 +20,7 @@
BTC
- +
@@ -28,7 +28,7 @@
sats
- + From a1968e01e56fc79eaa3717e139e89edd30aa317e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 13 Sep 2024 17:49:29 +0000 Subject: [PATCH 24/94] Add utxo chart to address page --- .../components/address/address.component.html | 14 + .../components/address/address.component.ts | 57 +++- .../utxo-graph/utxo-graph.component.html | 21 ++ .../utxo-graph/utxo-graph.component.scss | 59 ++++ .../utxo-graph/utxo-graph.component.ts | 285 ++++++++++++++++++ frontend/src/app/graphs/echarts.ts | 5 +- frontend/src/app/graphs/graphs.module.ts | 2 + .../src/app/interfaces/electrs.interface.ts | 7 + .../src/app/services/electrs-api.service.ts | 12 +- frontend/src/app/shared/common.utils.ts | 29 ++ 10 files changed, 483 insertions(+), 8 deletions(-) create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.html create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.scss create mode 100644 frontend/src/app/components/utxo-graph/utxo-graph.component.ts diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 31dff2fa5..b893d7e22 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -94,6 +94,20 @@ + +
+
+

Unspent Outputs

+
+
+
+
+ +
+
+
+
+

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

{{ error }}

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

-
-
ETA
-
- - - @if (eta.blocks >= 7) { - Not any time soon - } @else { - - } - - - - - -
-
+ @if (!replaced) { +
+
ETA
+
+ + + @if (eta.blocks >= 7) { + Not any time soon + } @else { + + } + + + + + +
+
+ } } @else if (tx && tx.status?.confirmed) {
Confirmed at
From 99290a7946b96a11dcf519ddcafee4a777d9d782 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 17 Sep 2024 14:34:18 +0200 Subject: [PATCH 27/94] Show http error in pizza tracker --- .../src/app/components/tracker/tracker.component.html | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/tracker/tracker.component.html b/frontend/src/app/components/tracker/tracker.component.html index d467aae80..7cb100cf7 100644 --- a/frontend/src/app/components/tracker/tracker.component.html +++ b/frontend/src/app/components/tracker/tracker.component.html @@ -42,7 +42,7 @@
-
+
@if (replaced) {
-
+
@if (isLoading) {
@@ -184,6 +184,12 @@
}
+ +
+ + Error loading transaction data. + +
diff --git a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html index 07bcdc2f1..0f436f9ac 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline-tooltip.component.html @@ -21,14 +21,14 @@ Fee - {{ accelerationInfo.fee | number }} sat + {{ accelerationInfo.fee | number }} sats Out-of-band fees @if (accelerationInfo.status === 'accelerated') { - {{ accelerationInfo.feeDelta | number }} sat + {{ accelerationInfo.feeDelta | number }} sats } @else { - {{ accelerationInfo.bidBoost | number }} sat + {{ accelerationInfo.bidBoost | number }} sats } diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html index 8bdd4f14d..ffd8e9c3d 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.html @@ -33,7 +33,7 @@ - {{ (acceleration.feeDelta) | number }} sat + {{ (acceleration.feeDelta) | number }} sats @@ -41,7 +41,7 @@ - {{ acceleration.boost | number }} sat + {{ acceleration.boost | number }} sats ~ diff --git a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html index f1f5bb3d4..f8fb3c89d 100644 --- a/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html +++ b/frontend/src/app/components/block-overview-tooltip/block-overview-tooltip.component.html @@ -40,7 +40,7 @@ Fee - {{ fee | number }} sat   + {{ fee | number }} sats   Fee rate diff --git a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html index 46cda0488..19c08bad9 100644 --- a/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html +++ b/frontend/src/app/components/rbf-timeline/rbf-timeline-tooltip.component.html @@ -19,7 +19,7 @@ Fee - {{ rbfInfo.tx.fee | number }} sat + {{ rbfInfo.tx.fee | number }} sats Virtual size diff --git a/frontend/src/app/components/transaction/transaction-preview.component.html b/frontend/src/app/components/transaction/transaction-preview.component.html index 63a11a8f0..066e0d442 100644 --- a/frontend/src/app/components/transaction/transaction-preview.component.html +++ b/frontend/src/app/components/transaction/transaction-preview.component.html @@ -21,7 +21,7 @@ ‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }} - Fee {{ tx.fee | number }} sat + Fee {{ tx.fee | number }} sats
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index b2e55a3b0..c0f5c6103 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -606,9 +606,9 @@ @if (!isLoadingTx) { Fee - {{ tx.fee | number }} sat + {{ tx.fee | number }} sats @if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) { - +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sat + +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} sats } diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 8954e4ecb..9b88678b4 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -321,7 +321,7 @@
 – {{ tx.fee | number }} sat + i18n="shared.sats">sats
Show more inputs to reveal fee data
From 72a5f4a521177c9cc60cb4f7b779708b5e791215 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 23 Sep 2024 00:18:59 +0800 Subject: [PATCH 41/94] amount selector sat -> sats --- .../components/amount-selector/amount-selector.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/amount-selector/amount-selector.component.html b/frontend/src/app/components/amount-selector/amount-selector.component.html index b509d6fe3..a16a24d4f 100644 --- a/frontend/src/app/components/amount-selector/amount-selector.component.html +++ b/frontend/src/app/components/amount-selector/amount-selector.component.html @@ -1,7 +1,7 @@
From 06e699e52b38b051712a2f4775d37887c808bb3a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Sep 2024 16:49:08 +0000 Subject: [PATCH 42/94] address utxo chart color by age & updates --- .../components/address/address.component.ts | 39 +++++ .../components/block-overview-graph/utils.ts | 13 ++ .../src/app/components/time/time.component.ts | 161 +++++++++++------- .../utxo-graph/utxo-graph.component.ts | 68 +++++++- frontend/src/app/shared/common.utils.ts | 4 +- 5 files changed, 211 insertions(+), 74 deletions(-) diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 5ce82ef8c..aaf480d8e 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -319,6 +319,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.transactions.slice(); this.mempoolStats.removeTx(transaction); this.audioService.playSound('magic'); + this.confirmTransaction(tx); } else { if (this.addTransaction(transaction, false)) { this.audioService.playSound('magic'); @@ -345,10 +346,12 @@ export class AddressComponent implements OnInit, OnDestroy { } // update utxos in-place + let utxosChanged = false; for (const vin of transaction.vin) { const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); if (utxoIndex !== -1) { this.utxos.splice(utxoIndex, 1); + utxosChanged = true; } } for (const [index, vout] of transaction.vout.entries()) { @@ -359,8 +362,12 @@ export class AddressComponent implements OnInit, OnDestroy { value: vout.value, status: JSON.parse(JSON.stringify(transaction.status)), }); + utxosChanged = true; } } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } return true; } @@ -374,6 +381,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.transactions.slice(); // update utxos in-place + let utxosChanged = false; for (const vin of transaction.vin) { if (vin.prevout?.scriptpubkey_address === this.address.address) { this.utxos.push({ @@ -382,6 +390,7 @@ export class AddressComponent implements OnInit, OnDestroy { value: vin.prevout.value, status: { confirmed: true }, // Assuming the input was confirmed }); + utxosChanged = true; } } for (const [index, vout] of transaction.vout.entries()) { @@ -389,13 +398,43 @@ export class AddressComponent implements OnInit, OnDestroy { const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); if (utxoIndex !== -1) { this.utxos.splice(utxoIndex, 1); + utxosChanged = true; } } } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } return true; } + confirmTransaction(transaction: Transaction): void { + // update utxos in-place + let utxosChanged = false; + for (const vin of transaction.vin) { + if (vin.prevout?.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === vin.txid && utxo.vout === vin.vout); + if (utxoIndex !== -1) { + this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + for (const [index, vout] of transaction.vout.entries()) { + if (vout.scriptpubkey_address === this.address.address) { + const utxoIndex = this.utxos.findIndex((utxo) => utxo.txid === transaction.txid && utxo.vout === index); + if (utxoIndex !== -1) { + this.utxos[utxoIndex].status = JSON.parse(JSON.stringify(transaction.status)); + utxosChanged = true; + } + } + } + if (utxosChanged) { + this.utxos = this.utxos.slice(); + } + } + loadMore(): void { if (this.isLoadingTransactions || this.fullyLoaded) { return; diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index 625029db0..287c4bf34 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -11,6 +11,10 @@ export function hexToColor(hex: string): Color { }; } +export function colorToHex(color: Color): string { + return [color.r, color.g, color.b].map(c => Math.round(c * 255).toString(16)).join(''); +} + export function desaturate(color: Color, amount: number): Color { const gray = (color.r + color.g + color.b) / 6; return { @@ -30,6 +34,15 @@ export function darken(color: Color, amount: number): Color { }; } +export function mix(color1: Color, color2: Color, amount: number): Color { + return { + r: color1.r * (1 - amount) + color2.r * amount, + g: color1.g * (1 - amount) + color2.g * amount, + b: color1.b * (1 - amount) + color2.b * amount, + a: color1.a * (1 - amount) + color2.a * amount, + }; +} + export function setOpacity(color: Color, opacity: number): Color { return { ...color, diff --git a/frontend/src/app/components/time/time.component.ts b/frontend/src/app/components/time/time.component.ts index 3015007b2..f0c73c80b 100644 --- a/frontend/src/app/components/time/time.component.ts +++ b/frontend/src/app/components/time/time.component.ts @@ -3,6 +3,28 @@ import { StateService } from '../../services/state.service'; import { dates } from '../../shared/i18n/dates'; import { DatePipe } from '@angular/common'; +const datePipe = new DatePipe(navigator.language || 'en-US'); + +const intervals = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + second: 1 +}; + +const precisionThresholds = { + year: 100, + month: 18, + week: 12, + day: 31, + hour: 48, + minute: 90, + second: 90 +}; + @Component({ selector: 'app-time', templateUrl: './time.component.html', @@ -12,19 +34,9 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { interval: number; text: string; tooltip: string; - precisionThresholds = { - year: 100, - month: 18, - week: 12, - day: 31, - hour: 48, - minute: 90, - second: 90 - }; - intervals = {}; @Input() time: number; - @Input() dateString: number; + @Input() dateString: string; @Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain'; @Input() fastRender = false; @Input() fixedRender = false; @@ -40,37 +52,25 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { constructor( private ref: ChangeDetectorRef, private stateService: StateService, - private datePipe: DatePipe, - ) { - this.intervals = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - second: 1 - }; - } + ) {} ngOnInit() { + this.calculateTime(); if(this.fixedRender){ - this.text = this.calculate(); return; } if (!this.stateService.isBrowser) { - this.text = this.calculate(); this.ref.markForCheck(); return; } this.interval = window.setInterval(() => { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); }, 1000 * (this.fastRender ? 1 : 60)); } ngOnChanges() { - this.text = this.calculate(); + this.calculateTime(); this.ref.markForCheck(); } @@ -78,40 +78,71 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { clearInterval(this.interval); } - calculate() { - if (this.time == null) { - return; + calculateTime(): void { + const { text, tooltip } = TimeComponent.calculate( + this.time, + this.kind, + this.relative, + this.precision, + this.minUnit, + this.showTooltip, + this.units, + this.dateString, + this.lowercaseStart, + this.numUnits, + this.fractionDigits, + ); + this.text = text; + this.tooltip = tooltip; + } + + static calculate( + time: number, + kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within', + relative: boolean = false, + precision: number = 0, + minUnit: 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' = 'second', + showTooltip: boolean = false, + units: string[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'], + dateString?: string, + lowercaseStart: boolean = false, + numUnits: number = 1, + fractionDigits: number = 0, + ): { text: string, tooltip: string } { + if (time == null) { + return { text: '', tooltip: '' }; } let seconds: number; - switch (this.kind) { + let tooltip: string = ''; + switch (kind) { case 'since': - seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000); - this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm'); + seconds = Math.floor((+new Date() - +new Date(dateString || time * 1000)) / 1000); + tooltip = datePipe.transform(new Date(dateString || time * 1000), 'yyyy-MM-dd HH:mm'); break; case 'until': case 'within': - seconds = (+new Date(this.time) - +new Date()) / 1000; - this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm'); + seconds = (+new Date(time) - +new Date()) / 1000; + tooltip = datePipe.transform(new Date(time), 'yyyy-MM-dd HH:mm'); break; default: - seconds = Math.floor(this.time); - this.tooltip = ''; + seconds = Math.floor(time); + tooltip = ''; } - if (!this.showTooltip || this.relative) { - this.tooltip = ''; + if (!showTooltip || relative) { + tooltip = ''; } - if (seconds < 1 && this.kind === 'span') { - return $localize`:@@date-base.immediately:Immediately`; + if (seconds < 1 && kind === 'span') { + return { tooltip, text: $localize`:@@date-base.immediately:Immediately` }; } else if (seconds < 60) { - if (this.relative || this.kind === 'since') { - if (this.lowercaseStart) { - return $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1); + if (relative || kind === 'since') { + if (lowercaseStart) { + return { tooltip, text: $localize`:@@date-base.just-now:Just now`.charAt(0).toLowerCase() + $localize`:@@date-base.just-now:Just now`.slice(1) }; } - return $localize`:@@date-base.just-now:Just now`; - } else if (this.kind === 'until' || this.kind === 'within') { + return { tooltip, text: $localize`:@@date-base.just-now:Just now` }; + } else if (kind === 'until' || kind === 'within') { seconds = 60; } } @@ -119,44 +150,44 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy { let counter: number; const result = []; let usedUnits = 0; - for (const [index, unit] of this.units.entries()) { - let precisionUnit = this.units[Math.min(this.units.length - 1, index + this.precision)]; - counter = Math.floor(seconds / this.intervals[unit]); - const precisionCounter = Math.round(seconds / this.intervals[precisionUnit]); - if (precisionCounter > this.precisionThresholds[precisionUnit]) { + for (const [index, unit] of units.entries()) { + let precisionUnit = units[Math.min(units.length - 1, index + precision)]; + counter = Math.floor(seconds / intervals[unit]); + const precisionCounter = Math.round(seconds / intervals[precisionUnit]); + if (precisionCounter > precisionThresholds[precisionUnit]) { precisionUnit = unit; } - if (this.units.indexOf(precisionUnit) === this.units.indexOf(this.minUnit)) { + if (units.indexOf(precisionUnit) === units.indexOf(minUnit)) { counter = Math.max(1, counter); } if (counter > 0) { let rounded; - const roundFactor = Math.pow(10,this.fractionDigits || 0); - if ((this.kind === 'until' || this.kind === 'within') && usedUnits < this.numUnits) { - rounded = Math.floor((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; + const roundFactor = Math.pow(10,fractionDigits || 0); + if ((kind === 'until' || kind === 'within') && usedUnits < numUnits) { + rounded = Math.floor((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; } else { - rounded = Math.round((seconds / this.intervals[precisionUnit]) * roundFactor) / roundFactor; + rounded = Math.round((seconds / intervals[precisionUnit]) * roundFactor) / roundFactor; } - if ((this.kind !== 'until' && this.kind !== 'within')|| this.numUnits === 1) { - return this.formatTime(this.kind, precisionUnit, rounded); + if ((kind !== 'until' && kind !== 'within')|| numUnits === 1) { + return { tooltip, text: TimeComponent.formatTime(kind, precisionUnit, rounded) }; } else { if (!usedUnits) { - result.push(this.formatTime(this.kind, precisionUnit, rounded)); + result.push(TimeComponent.formatTime(kind, precisionUnit, rounded)); } else { - result.push(this.formatTime('', precisionUnit, rounded)); + result.push(TimeComponent.formatTime('', precisionUnit, rounded)); } - seconds -= (rounded * this.intervals[precisionUnit]); + seconds -= (rounded * intervals[precisionUnit]); usedUnits++; - if (usedUnits >= this.numUnits) { - return result.join(', '); + if (usedUnits >= numUnits) { + return { tooltip, text: result.join(', ') }; } } } } - return result.join(', '); + return { tooltip, text: result.join(', ') }; } - private formatTime(kind, unit, number): string { + static formatTime(kind, unit, number): string { const dateStrings = dates(number); switch (kind) { case 'since': diff --git a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts index 5e034a700..91dc70240 100644 --- a/frontend/src/app/components/utxo-graph/utxo-graph.component.ts +++ b/frontend/src/app/components/utxo-graph/utxo-graph.component.ts @@ -6,6 +6,14 @@ import { StateService } from '../../services/state.service'; import { Router } from '@angular/router'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { renderSats } from '../../shared/common.utils'; +import { colorToHex, hexToColor, mix } from '../block-overview-graph/utils'; +import { TimeComponent } from '../time/time.component'; + +const newColorHex = '1bd8f4'; +const oldColorHex = '9339f4'; +const pendingColorHex = 'eba814'; +const newColor = hexToColor(newColorHex); +const oldColor = hexToColor(oldColorHex); @Component({ selector: 'app-utxo-graph', @@ -29,7 +37,8 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { @Input() widget: boolean = false; subscription: Subscription; - redraw$: BehaviorSubject = new BehaviorSubject(false); + lastUpdate: number = 0; + updateInterval; chartOptions: EChartsOption = {}; chartInitOptions = { @@ -46,7 +55,14 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { private zone: NgZone, private router: Router, private relativeUrlPipe: RelativeUrlPipe, - ) {} + ) { + // re-render the chart every 10 seconds, to keep the age colors up to date + this.updateInterval = setInterval(() => { + if (this.lastUpdate < Date.now() - 10000 && this.utxos) { + this.prepareChartOptions(this.utxos); + } + }, 10000); + } ngOnChanges(changes: SimpleChanges): void { this.isLoading = true; @@ -82,7 +98,18 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { // Naive algorithm to pack circles as tightly as possible without overlaps const placedCircles: { x: number, y: number, r: number, utxo: Utxo, distances: number[] }[] = []; // Pack in descending order of value, and limit to the top 500 to preserve performance - const sortedUtxos = utxos.sort((a, b) => b.value - a.value).slice(0, 500); + const sortedUtxos = utxos.sort((a, b) => { + if (a.value === b.value) { + if (a.status.confirmed && !b.status.confirmed) { + return -1; + } else if (!a.status.confirmed && b.status.confirmed) { + return 1; + } else { + return a.status.block_height - b.status.block_height; + } + } + return b.value - a.value; + }).slice(0, 500); let centerOfMass = { x: 0, y: 0 }; let weightOfMass = 0; sortedUtxos.forEach((utxo, index) => { @@ -192,7 +219,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { const x = datum[2] as number; const y = datum[3] as number; const r = datum[4] as number; - if (r * scale < 3) { + if (r * scale < 2) { // skip items too small to render cleanly return; } @@ -207,7 +234,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { r: (r * scale) - 1, }, style: { - fill: '#5470c6', + fill: '#' + this.getColor(utxo), } }, ]; @@ -230,7 +257,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { type: 'group', children: elements, }; - } + }, }], tooltip: { backgroundColor: 'rgba(17, 19, 31, 1)', @@ -247,14 +274,40 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { return ` ${utxo.txid.slice(0, 6)}...${utxo.txid.slice(-6)}:${utxo.vout}
- ${valueStr}`; + ${valueStr} +
+ ${utxo.status.confirmed ? 'Confirmed ' + TimeComponent.calculate(utxo.status.block_time, 'since', true, 1, 'minute').text : 'Pending'} + `; }, } }; + this.lastUpdate = Date.now(); this.cd.markForCheck(); } + getColor(utxo: Utxo): string { + if (utxo.status.confirmed) { + const age = Date.now() / 1000 - utxo.status.block_time; + const oneHour = 60 * 60; + const fourYears = 4 * 365 * 24 * 60 * 60; + + if (age < oneHour) { + return newColorHex; + } else if (age >= fourYears) { + return oldColorHex; + } else { + // Logarithmic scale between 1 hour and 4 years + const logAge = Math.log(age / oneHour); + const logMax = Math.log(fourYears / oneHour); + const t = logAge / logMax; + return colorToHex(mix(newColor, oldColor, t)); + } + } else { + return pendingColorHex; + } + } + onChartClick(e): void { if (e.data?.[0]?.txid) { this.zone.run(() => { @@ -277,6 +330,7 @@ export class UtxoGraphComponent implements OnChanges, OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } + clearInterval(this.updateInterval); } isMobile(): boolean { diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 6bdc3262b..5ccb369f6 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -204,12 +204,12 @@ export function renderSats(value: number, network: string, mode: 'sats' | 'btc' break; } if (mode === 'btc' || (mode === 'auto' && value >= 1000000)) { - return `${amountShortenerPipe.transform(value / 100000000)} ${prefix}BTC`; + return `${amountShortenerPipe.transform(value / 100000000, 2)} ${prefix}BTC`; } else { if (prefix.length) { prefix += '-'; } - return `${amountShortenerPipe.transform(value)} ${prefix}sats`; + return `${amountShortenerPipe.transform(value, 2)} ${prefix}sats`; } } From e144e139b70bec3af496725a88706d41179912d6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 22 Sep 2024 18:06:55 +0000 Subject: [PATCH 43/94] Update accelerating pie chart in real time --- .../active-acceleration-box.component.html | 4 ++-- .../active-acceleration-box.component.ts | 12 ++++++++---- .../transaction/transaction.component.html | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html index 13d38443e..dbc79fb95 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.html @@ -10,10 +10,10 @@
- @if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) { + @if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) { } @else { - + }
diff --git a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts index f95bb71c8..fb727c1a4 100644 --- a/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts +++ b/frontend/src/app/components/acceleration/active-acceleration-box/active-acceleration-box.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Transaction } from '../../../interfaces/electrs.interface'; import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; @@ -23,7 +23,8 @@ function toRGB({r,g,b}): string { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ActiveAccelerationBox implements OnChanges { - @Input() tx: Transaction; + @Input() acceleratedBy?: number[]; + @Input() effectiveFeeRate?: number; @Input() accelerationInfo: Acceleration; @Input() miningStats: MiningStats; @Input() pools: number[]; @@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges { timespan = ''; chartInstance: any = undefined; - constructor() {} + constructor( + private cd: ChangeDetectorRef, + ) {} ngOnChanges(changes: SimpleChanges): void { - const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy; + const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy; if (pools && this.miningStats) { this.prepareChartOptions(pools); } @@ -132,6 +135,7 @@ export class ActiveAccelerationBox implements OnChanges { } ] }; + this.cd.markForCheck(); } onChartInit(ec) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index c0f5c6103..9d3c0d678 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -670,7 +670,7 @@ - + From 4220f99477f8e337478e68c5f647dd3c6228f083 Mon Sep 17 00:00:00 2001 From: BitcoinMechanic Date: Sun, 22 Sep 2024 14:46:53 -0700 Subject: [PATCH 44/94] remove 'on'/UI changes per feedback --- backend/src/utils/bitcoin-script.ts | 3 ++- .../app/components/block/block.component.html | 1 - .../app/components/block/block.component.scss | 6 ------ .../blockchain-blocks.component.html | 21 +++++++++++-------- .../blockchain-blocks.component.scss | 17 ++++++++------- .../blockchain/blockchain.component.scss | 2 +- .../transaction/transaction.component.html | 1 - .../transaction/transaction.component.scss | 6 ------ .../app/dashboard/dashboard.component.scss | 2 +- 9 files changed, 25 insertions(+), 34 deletions(-) diff --git a/backend/src/utils/bitcoin-script.ts b/backend/src/utils/bitcoin-script.ts index 619f1876d..b43b7a72d 100644 --- a/backend/src/utils/bitcoin-script.ts +++ b/backend/src/utils/bitcoin-script.ts @@ -220,7 +220,8 @@ export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null const tagStart = tagLengthByte + 1; const tags = bytes.slice(tagStart, tagStart + tagsLength); - const tagString = String.fromCharCode(...tags); + let tagString = String.fromCharCode(...tags); + tagString = tagString.replace('\x00', ''); return tagString.split('\x0f'); } \ No newline at end of file diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index d97ebafc5..46900179b 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -185,7 +185,6 @@
{{ block.extras.pool.minerNames[1] }}
- on {{ block.extras.pool.name }}
diff --git a/frontend/src/app/components/block/block.component.scss b/frontend/src/app/components/block/block.component.scss index 887d7281f..6eae3fe3a 100644 --- a/frontend/src/app/components/block/block.component.scss +++ b/frontend/src/app/components/block/block.component.scss @@ -94,12 +94,6 @@ h1 { border-radius: .25rem; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .pool-logo { width: 25px; height: 25px; diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html index 3fdafb540..79b9cea62 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.html @@ -61,18 +61,21 @@
-
- {{ block.extras.pool.minerNames[1] }} - diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss index b03b3d3cb..a0111215a 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.scss @@ -19,12 +19,6 @@ pointer-events: none; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .on-pool-name-text { display: inline-block; padding-top: 2px; @@ -42,10 +36,17 @@ } .on-pool-container { + align-items: center; + position: relative; + top: -8px; display: flex; flex-direction: column; } +.on-pool-container.selected { + top: 0px; +} + .pool-container { margin-top: 12px; } @@ -156,7 +157,7 @@ #arrow-up { position: relative; left: calc(var(--block-size) * 0.6); - top: calc(var(--block-size) * 1.38); + top: calc(var(--block-size) * 1.28); width: 0; height: 0; border-left: calc(var(--block-size) * 0.2) solid transparent; @@ -186,7 +187,7 @@ .badge { position: relative; - top: 8px; + top: 15px; z-index: 101; color: #FFF; } diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index 7f98f5ed1..32225598a 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -14,7 +14,7 @@ } .blockchain-wrapper { - height: 272px; + height: 260px; -webkit-user-select: none; /* Safari */ -moz-user-select: none; /* Firefox */ -ms-user-select: none; /* IE10+/Edge */ diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index d00ab0e02..32eb10f8e 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -687,7 +687,6 @@
{{ pool.minerNames[1] }}
- on {{ pool.name }}
diff --git a/frontend/src/app/components/transaction/transaction.component.scss b/frontend/src/app/components/transaction/transaction.component.scss index 43cece726..40b813cae 100644 --- a/frontend/src/app/components/transaction/transaction.component.scss +++ b/frontend/src/app/components/transaction/transaction.component.scss @@ -73,12 +73,6 @@ border-radius: .25rem; } -.on-pool-text { - font-weight: normal; - color: gray; - padding-inline-end: 4px; -} - .pool-logo { width: 25px; height: 25px; diff --git a/frontend/src/app/dashboard/dashboard.component.scss b/frontend/src/app/dashboard/dashboard.component.scss index 0864f0096..9ad09981f 100644 --- a/frontend/src/app/dashboard/dashboard.component.scss +++ b/frontend/src/app/dashboard/dashboard.component.scss @@ -1,6 +1,6 @@ .dashboard-container { text-align: center; - margin-top: 1.0rem; + margin-top: 0.5rem; .col { margin-bottom: 1.5rem; } From 0e5698955fef808b8e66e86176abefcfff3842b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 02:09:59 +0000 Subject: [PATCH 45/94] Bump esbuild from 0.23.0 to 0.24.0 in /frontend Bumps [esbuild](https://github.com/evanw/esbuild) from 0.23.0 to 0.24.0. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: esbuild dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- frontend/package-lock.json | 398 ++++++++++++++++++------------------- frontend/package.json | 2 +- 2 files changed, 200 insertions(+), 200 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b53f80c88..2b10d398a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,7 +34,7 @@ "clipboard": "^2.0.11", "domino": "^2.1.6", "echarts": "~5.5.0", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "lightweight-charts": "~3.8.0", "ngx-echarts": "~17.2.0", "ngx-infinite-scroll": "^17.0.0", @@ -3201,9 +3201,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], @@ -3216,9 +3216,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -3231,9 +3231,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -3246,9 +3246,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -3261,9 +3261,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -3276,9 +3276,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -3291,9 +3291,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -3306,9 +3306,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -3321,9 +3321,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -3336,9 +3336,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -3351,9 +3351,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -3366,9 +3366,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -3381,9 +3381,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -3396,9 +3396,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -3411,9 +3411,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -3426,9 +3426,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -3441,9 +3441,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -3456,9 +3456,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -3471,9 +3471,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "cpu": [ "arm64" ], @@ -3486,9 +3486,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -3501,9 +3501,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -3516,9 +3516,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -3531,9 +3531,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -3546,9 +3546,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -9210,9 +9210,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -9221,30 +9221,30 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/esbuild-wasm": { @@ -20616,147 +20616,147 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" }, "@esbuild/aix-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.0.tgz", - "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "optional": true }, "@esbuild/android-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.0.tgz", - "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "optional": true }, "@esbuild/android-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.0.tgz", - "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "optional": true }, "@esbuild/android-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.0.tgz", - "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", - "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "optional": true }, "@esbuild/darwin-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.0.tgz", - "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.0.tgz", - "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.0.tgz", - "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "optional": true }, "@esbuild/linux-arm": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.0.tgz", - "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "optional": true }, "@esbuild/linux-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.0.tgz", - "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "optional": true }, "@esbuild/linux-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.0.tgz", - "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "optional": true }, "@esbuild/linux-loong64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.0.tgz", - "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.0.tgz", - "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.0.tgz", - "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.0.tgz", - "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "optional": true }, "@esbuild/linux-s390x": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.0.tgz", - "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "optional": true }, "@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.0.tgz", - "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.0.tgz", - "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.0.tgz", - "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "optional": true }, "@esbuild/sunos-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.0.tgz", - "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "optional": true }, "@esbuild/win32-arm64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.0.tgz", - "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "optional": true }, "@esbuild/win32-ia32": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.0.tgz", - "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "optional": true }, "@esbuild/win32-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz", - "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "optional": true }, "@eslint-community/eslint-utils": { @@ -25088,34 +25088,34 @@ } }, "esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "requires": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "esbuild-wasm": { diff --git a/frontend/package.json b/frontend/package.json index 3b5d61be0..415ac74fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -92,7 +92,7 @@ "ngx-infinite-scroll": "^17.0.0", "qrcode": "1.5.1", "rxjs": "~7.8.1", - "esbuild": "^0.23.0", + "esbuild": "^0.24.0", "tinyify": "^4.0.0", "tlite": "^0.1.9", "tslib": "~2.7.0", From 05e88a25be1f4ce4a1bba9f2229cdaed0af64506 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 23 Sep 2024 14:15:00 +0800 Subject: [PATCH 46/94] npm audit fix --- frontend/package-lock.json | 211 ++++++++++--------------------------- 1 file changed, 55 insertions(+), 156 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b10d398a..9d4e018ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8884,9 +8884,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "devOptional": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -8897,60 +8897,30 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "devOptional": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" } }, - "node_modules/engine.io-client/node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true, "engines": { "node": ">=10.0.0" @@ -8965,27 +8935,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -13406,9 +13355,9 @@ "optional": true }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "dependencies": { "isarray": "0.0.1" @@ -15952,33 +15901,13 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "dependencies": { - "ws": "~8.11.0" - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "node_modules/socket.io-client": { @@ -17860,12 +17789,12 @@ } }, "node_modules/wait-on/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -18337,9 +18266,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, @@ -24836,9 +24765,9 @@ } }, "engine.io": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", - "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "devOptional": true, "requires": { "@types/cookie": "^0.4.1", @@ -24849,8 +24778,8 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.1.0", - "ws": "~8.11.0" + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" }, "dependencies": { "cookie": { @@ -24858,48 +24787,26 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} } } }, "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "devOptional": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", + "ws": "~8.17.1", "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", - "devOptional": true - }, - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } } }, "engine.io-parser": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", - "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "devOptional": true }, "enhanced-resolve": { @@ -28210,9 +28117,9 @@ "optional": true }, "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "optional": true, "requires": { "isarray": "0.0.1" @@ -30070,21 +29977,13 @@ } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "devOptional": true, "requires": { - "ws": "~8.11.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "devOptional": true, - "requires": {} - } + "debug": "~4.3.4", + "ws": "~8.17.1" } }, "socket.io-client": { @@ -31339,12 +31238,12 @@ }, "dependencies": { "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "optional": true, "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -31674,9 +31573,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xhr2": { From 2a9346f695ea52608edb6cda26f9b331281248ef Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 23 Sep 2024 14:47:57 +0200 Subject: [PATCH 47/94] 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 48/94] 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 49/94] 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 50/94] 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 51/94] 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 52/94] 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 53/94] 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 54/94] 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 55/94] 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 56/94] 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 57/94] 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 58/94] 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 @@
- + +
@@ -96,6 +97,15 @@ + + + + + + @@ -236,7 +246,12 @@ - OP_RETURN {{ vout.scriptpubkey_asm | hex2ascii }} + OP_RETURN  + @if (vout.isRunestone) { + + } @else { + {{ vout.scriptpubkey_asm | hex2ascii }} + } {{ vout.scriptpubkey_type | scriptpubkeyType }} @@ -276,6 +291,15 @@ + + + +
+ +
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss index 280e36b0f..335464060 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.scss +++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss @@ -175,4 +175,15 @@ h2 { .witness-item { overflow: hidden; } -} \ No newline at end of file +} + +.badge-ord { + background-color: var(--grey); + position: relative; + top: -2px; + font-size: 81%; + border: 0; + &.primary { + background-color: var(--primary); + } +} diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 316a6ab85..1f45d5241 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -11,6 +11,10 @@ import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { PriceService } from '../../services/price.service'; import { StorageService } from '../../services/storage.service'; +import { OrdApiService } from '../../services/ord-api.service'; +import { Inscription } from '../ord-data/ord-data.component'; +import { Runestone } from '../../shared/ord/rune/runestone'; +import { Etching } from '../../shared/ord/rune/etching'; @Component({ selector: 'app-transactions-list', @@ -50,12 +54,14 @@ export class TransactionsListComponent implements OnInit, OnChanges { outputRowLimit: number = 12; showFullScript: { [vinIndex: number]: boolean } = {}; showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {}; + showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {}; constructor( public stateService: StateService, private cacheService: CacheService, private electrsApiService: ElectrsApiService, private apiService: ApiService, + private ordApiService: OrdApiService, private assetsService: AssetsService, private ref: ChangeDetectorRef, private priceService: PriceService, @@ -239,6 +245,24 @@ export class TransactionsListComponent implements OnInit, OnChanges { tap((price) => tx['price'] = price), ).subscribe(); } + + // Check for ord data fingerprints in inputs and outputs + if (this.stateService.network !== 'liquid' && this.stateService.network !== 'liquidtestnet') { + for (let i = 0; i < tx.vin.length; i++) { + if (tx.vin[i].prevout?.scriptpubkey_type === 'v1_p2tr' && tx.vin[i].witness?.length) { + const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50'); + if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) { + tx.vin[i].isInscription = true; + } + } + } + for (let i = 0; i < tx.vout.length; i++) { + if (tx.vout[i]?.scriptpubkey?.startsWith('6a5d')) { + tx.vout[i].isRunestone = true; + break; + } + } + } }); if (this.blockTime && this.transactions?.length && this.currency) { @@ -372,6 +396,40 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.showFullWitness[vinIndex][witnessIndex] = !this.showFullWitness[vinIndex][witnessIndex]; } + toggleOrdData(txid: string, type: 'vin' | 'vout', index: number) { + const tx = this.transactions.find((tx) => tx.txid === txid); + if (!tx) { + return; + } + + const key = tx.txid + '-' + type + '-' + index; + this.showOrdData[key] = this.showOrdData[key] || { show: false }; + + if (type === 'vin') { + + if (!this.showOrdData[key].inscriptions) { + const hasAnnex = tx.vin[index].witness?.[tx.vin[index].witness.length - 1].startsWith('50'); + this.showOrdData[key].inscriptions = this.ordApiService.decodeInscriptions(tx.vin[index].witness[tx.vin[index].witness.length - (hasAnnex ? 3 : 2)]); + } + this.showOrdData[key].show = !this.showOrdData[key].show; + + } else if (type === 'vout') { + + if (!this.showOrdData[key].runestone) { + this.ordApiService.decodeRunestone$(tx).pipe( + tap((runestone) => { + if (runestone) { + Object.assign(this.showOrdData[key], runestone); + this.ref.markForCheck(); + } + }), + ).subscribe(); + } + this.showOrdData[key].show = !this.showOrdData[key].show; + + } + } + ngOnDestroy(): void { this.outspendsSubscription.unsubscribe(); this.currencyChangeSubscription?.unsubscribe(); diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 5bc5bfc1d..95a749b60 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -74,6 +74,8 @@ export interface Vin { issuance?: Issuance; // Custom lazy?: boolean; + // Ord + isInscription?: boolean; } interface Issuance { @@ -98,6 +100,8 @@ export interface Vout { valuecommitment?: number; asset?: string; pegout?: Pegout; + // Ord + isRunestone?: boolean; } interface Pegout { diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index 8e991782b..f1468f8aa 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -107,6 +107,10 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'}); } + getBlockTxId$(hash: string, index: number): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txid/' + index, { responseType: 'text' }); + } + getAddress$(address: string): Observable
{ return this.httpClient.get
(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); } diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts new file mode 100644 index 000000000..bc726e839 --- /dev/null +++ b/frontend/src/app/services/ord-api.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core'; +import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; +import { Inscription } from '../components/ord-data/ord-data.component'; +import { Transaction } from '../interfaces/electrs.interface'; +import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils'; +import { Runestone } from '../shared/ord/rune/runestone'; +import { Etching } from '../shared/ord/rune/etching'; +import { ElectrsApiService } from './electrs-api.service'; +import { UNCOMMON_GOODS } from '../shared/ord/rune/runestone'; + +@Injectable({ + providedIn: 'root' +}) +export class OrdApiService { + + constructor( + private electrsApiService: ElectrsApiService, + ) { } + + decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { + const runestoneTx = { vout: tx.vout.map(vout => ({ scriptpubkey: vout.scriptpubkey })) }; + const decipher = Runestone.decipher(runestoneTx); + + // For now, ignore cenotaphs + let message = decipher.isSome() ? decipher.unwrap() : null; + if (message?.type === 'cenotaph') { + return of({ runestone: null, runeInfo: {} }); + } + + const runestone = message as Runestone; + const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; + const runesToFetch: Set = new Set(); + + if (runestone) { + if (runestone.mint.isSome()) { + const mint = runestone.mint.unwrap().toString(); + + if (mint === '1:0') { + runeInfo[mint] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; + } else { + runesToFetch.add(mint); + } + } + + if (runestone.edicts.length) { + runestone.edicts.forEach(edict => { + runesToFetch.add(edict.id.toString()); + }); + } + + if (runesToFetch.size) { + const runeEtchingObservables = Array.from(runesToFetch).map(runeId => { + return this.getEtchingFromRuneId$(runeId).pipe( + tap(etching => { + if (etching) { + runeInfo[runeId] = etching; + } + }) + ); + }); + + return forkJoin(runeEtchingObservables).pipe( + map(() => { + return { runestone: runestone, runeInfo }; + }) + ); + } + } + + return of({ runestone: runestone, runeInfo }); + } + + // Get etching from runeId by looking up the transaction that etched the rune + getEtchingFromRuneId$(runeId: string): Observable<{ etching: Etching; txid: string; }> { + const [blockNumber, txIndex] = runeId.split(':'); + + return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockNumber)).pipe( + switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), + switchMap(txId => this.electrsApiService.getTransaction$(txId)), + switchMap(tx => { + const decipheredMessage = Runestone.decipher(tx); + if (decipheredMessage.isSome()) { + const message = decipheredMessage.unwrap(); + if (message?.type === 'runestone' && message.etching.isSome()) { + return of({ etching: message.etching.unwrap(), txid: tx.txid }); + } + } + return of(null); + }), + catchError(() => of(null)) + ); + } + + decodeInscriptions(witness: string): Inscription[] | null { + + const inscriptions: Inscription[] = []; + const raw = hexToBytes(witness); + let startPosition = 0; + + while (true) { + const pointer = getNextInscriptionMark(raw, startPosition); + if (pointer === -1) break; + + const inscription = extractInscriptionData(raw, pointer); + if (inscription) { + inscriptions.push(inscription); + } + + startPosition = pointer; + } + + return inscriptions; + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 92b461548..25a60a70f 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -102,6 +102,7 @@ import { AccelerationsListComponent } from '../components/acceleration/accelerat import { PendingStatsComponent } from '../components/acceleration/pending-stats/pending-stats.component'; import { AccelerationStatsComponent } from '../components/acceleration/acceleration-stats/acceleration-stats.component'; import { AccelerationSparklesComponent } from '../components/acceleration/sparkles/acceleration-sparkles.component'; +import { OrdDataComponent } from '../components/ord-data/ord-data.component'; import { BlockViewComponent } from '../components/block-view/block-view.component'; import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component'; @@ -229,6 +230,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationStatsComponent, PendingStatsComponent, AccelerationSparklesComponent, + OrdDataComponent, HttpErrorComponent, TwitterWidgetComponent, FaucetComponent, @@ -361,6 +363,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerationStatsComponent, PendingStatsComponent, AccelerationSparklesComponent, + OrdDataComponent, HttpErrorComponent, TwitterWidgetComponent, TwitterLogin, From acae5a33b08fe4bd0edb7560cd41b0ac5a26f273 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 8 Oct 2024 01:41:35 +0000 Subject: [PATCH 76/94] replace rune parsing dependencies with minimal reimplementation --- .../ord-data/ord-data.component.html | 30 +- .../components/ord-data/ord-data.component.ts | 72 +--- .../transactions-list.component.ts | 7 +- frontend/src/app/services/ord-api.service.ts | 41 +- frontend/src/app/shared/ord/rune.utils.ts | 258 ++++++++++++ frontend/src/app/shared/ord/rune/artifact.ts | 4 - frontend/src/app/shared/ord/rune/cenotaph.ts | 14 - frontend/src/app/shared/ord/rune/constants.ts | 7 - frontend/src/app/shared/ord/rune/edict.ts | 34 -- frontend/src/app/shared/ord/rune/etching.ts | 54 --- frontend/src/app/shared/ord/rune/flag.ts | 20 - frontend/src/app/shared/ord/rune/flaw.ts | 12 - .../src/app/shared/ord/rune/integer/index.ts | 4 - .../src/app/shared/ord/rune/integer/u128.ts | 176 -------- .../src/app/shared/ord/rune/integer/u32.ts | 58 --- .../src/app/shared/ord/rune/integer/u64.ts | 58 --- .../src/app/shared/ord/rune/integer/u8.ts | 58 --- frontend/src/app/shared/ord/rune/message.ts | 67 --- frontend/src/app/shared/ord/rune/monads.ts | 392 ------------------ frontend/src/app/shared/ord/rune/rune.ts | 23 - frontend/src/app/shared/ord/rune/runeid.ts | 89 ---- frontend/src/app/shared/ord/rune/runestone.ts | 258 ------------ frontend/src/app/shared/ord/rune/script.ts | 237 ----------- frontend/src/app/shared/ord/rune/seekarray.ts | 43 -- .../src/app/shared/ord/rune/spacedrune.ts | 21 - frontend/src/app/shared/ord/rune/tag.ts | 60 --- frontend/src/app/shared/ord/rune/terms.ts | 9 - frontend/src/app/shared/ord/rune/utils.ts | 6 - 28 files changed, 300 insertions(+), 1812 deletions(-) create mode 100644 frontend/src/app/shared/ord/rune.utils.ts delete mode 100644 frontend/src/app/shared/ord/rune/artifact.ts delete mode 100644 frontend/src/app/shared/ord/rune/cenotaph.ts delete mode 100644 frontend/src/app/shared/ord/rune/constants.ts delete mode 100644 frontend/src/app/shared/ord/rune/edict.ts delete mode 100644 frontend/src/app/shared/ord/rune/etching.ts delete mode 100644 frontend/src/app/shared/ord/rune/flag.ts delete mode 100644 frontend/src/app/shared/ord/rune/flaw.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/index.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u128.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u32.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u64.ts delete mode 100644 frontend/src/app/shared/ord/rune/integer/u8.ts delete mode 100644 frontend/src/app/shared/ord/rune/message.ts delete mode 100644 frontend/src/app/shared/ord/rune/monads.ts delete mode 100644 frontend/src/app/shared/ord/rune/rune.ts delete mode 100644 frontend/src/app/shared/ord/rune/runeid.ts delete mode 100644 frontend/src/app/shared/ord/rune/runestone.ts delete mode 100644 frontend/src/app/shared/ord/rune/script.ts delete mode 100644 frontend/src/app/shared/ord/rune/seekarray.ts delete mode 100644 frontend/src/app/shared/ord/rune/spacedrune.ts delete mode 100644 frontend/src/app/shared/ord/rune/tag.ts delete mode 100644 frontend/src/app/shared/ord/rune/terms.ts delete mode 100644 frontend/src/app/shared/ord/rune/utils.ts diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html index be9a24715..696e7ea17 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.html +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -7,23 +7,23 @@ Mint {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} - + } - @if (totalSupply > -1) { - @if (premined > 0) { + @if (runestone?.etching?.supply) { + @if (runestone?.etching.premine > 0) { Premine - {{ premined >= 100000 ? (premined | amountShortener:undefined:undefined:true) : premined }} - {{ etchedSymbol }} - {{ etchedName }} - ({{ premined / totalSupply * 100 | amountShortener:0}}% of total supply) + {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} + ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) - } @else { + } @else { Etching of - {{ etchedSymbol }} - {{ etchedName }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} } } @@ -36,12 +36,6 @@ } - - @if (inscriptions?.length && type === 'vin') {
@@ -68,8 +62,8 @@ } - {{ runeInfo[id]?.etching.symbol.isSome() ? runeInfo[id]?.etching.symbol.unwrap() : '' }} + {{ runeInfo[id]?.etching.symbol || '' }} - {{ runeInfo[id]?.name }} + {{ runeInfo[id]?.etching.spacedName }} \ No newline at end of file diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index 8d7eef973..233b8d243 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -1,9 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { Runestone } from '../../shared/ord/rune/runestone'; -import { Etching } from '../../shared/ord/rune/etching'; -import { u128, u32, u8 } from '../../shared/ord/rune/integer'; import { HttpErrorResponse } from '@angular/common/http'; -import { SpacedRune } from '../../shared/ord/rune/spacedrune'; +import { Runestone, Etching } from '../../shared/ord/rune.utils'; export interface Inscription { body?: Uint8Array; @@ -22,79 +19,34 @@ export interface Inscription { export class OrdDataComponent implements OnChanges { @Input() inscriptions: Inscription[]; @Input() runestone: Runestone; - @Input() runeInfo: { [id: string]: { etching: Etching; txid: string; name?: string; } }; + @Input() runeInfo: { [id: string]: { etching: Etching; txid: string } }; @Input() error: HttpErrorResponse; @Input() type: 'vin' | 'vout'; + toNumber = (value: bigint): number => Number(value); + // Inscriptions inscriptionsData: { [key: string]: { count: number, totalSize: number, text?: string; json?: JSON; tag?: string; delegate?: string } }; // Rune mints minted: number; - // Rune etching - premined: number = -1; - totalSupply: number = -1; - etchedName: string; - etchedSymbol: string; // Rune transfers - transferredRunes: { key: string; etching: Etching; txid: string; name?: string; }[] = []; + transferredRunes: { key: string; etching: Etching; txid: string }[] = []; constructor() { } ngOnChanges(changes: SimpleChanges): void { if (changes.runestone && this.runestone) { - - Object.keys(this.runeInfo).forEach((key) => { - const rune = this.runeInfo[key].etching.rune.isSome() ? this.runeInfo[key].etching.rune.unwrap() : null; - const spacers = this.runeInfo[key].etching.spacers.isSome() ? this.runeInfo[key].etching.spacers.unwrap() : u32(0); - if (rune) { - this.runeInfo[key].name = new SpacedRune(rune, Number(spacers)).toString(); - } - this.transferredRunes.push({ key, ...this.runeInfo[key] }); - }); - - - if (this.runestone.mint.isSome() && this.runeInfo[this.runestone.mint.unwrap().toString()]) { - const mint = this.runestone.mint.unwrap().toString(); + this.transferredRunes = Object.entries(this.runeInfo).map(([key, runeInfo]) => ({ key, ...runeInfo })); + if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) { + const mint = this.runestone.mint.toString(); this.transferredRunes = this.transferredRunes.filter(rune => rune.key !== mint); - const terms = this.runeInfo[mint].etching.terms.isSome() ? this.runeInfo[mint].etching.terms.unwrap() : null; - let amount: u128; - if (terms) { - amount = terms.amount.isSome() ? terms.amount.unwrap() : u128(0); - } - const divisibility = this.runeInfo[mint].etching.divisibility.isSome() ? this.runeInfo[mint].etching.divisibility.unwrap() : u8(0); + const terms = this.runeInfo[mint].etching.terms; + const amount = terms?.amount; + const divisibility = this.runeInfo[mint].etching.divisibility; if (amount) { this.minted = this.getAmount(amount, divisibility); } } - - if (this.runestone.etching.isSome()) { - const etching = this.runestone.etching.unwrap(); - const rune = etching.rune.isSome() ? etching.rune.unwrap() : null; - const spacers = etching.spacers.isSome() ? etching.spacers.unwrap() : u32(0); - if (rune) { - this.etchedName = new SpacedRune(rune, Number(spacers)).toString(); - } - this.etchedSymbol = etching.symbol.isSome() ? etching.symbol.unwrap() : ''; - - const divisibility = etching.divisibility.isSome() ? etching.divisibility.unwrap() : u8(0); - const premine = etching.premine.isSome() ? etching.premine.unwrap() : u128(0); - if (premine) { - this.premined = this.getAmount(premine, divisibility); - } else { - this.premined = 0; - } - const terms = etching.terms.isSome() ? etching.terms.unwrap() : null; - let amount: u128; - if (terms) { - amount = terms.amount.isSome() ? terms.amount.unwrap() : u128(0); - if (amount) { - const cap = terms.cap.isSome() ? terms.cap.unwrap() : u128(0); - this.totalSupply = this.premined + this.getAmount(amount, divisibility) * Number(cap); - } - } else { - this.totalSupply = this.premined; - } - } } if (changes.inscriptions && this.inscriptions) { @@ -131,7 +83,7 @@ export class OrdDataComponent implements OnChanges { } } - getAmount(amount: u128 | bigint, divisibility: u8): number { + getAmount(amount: bigint, divisibility: number): number { const divisor = BigInt(10) ** BigInt(divisibility); const result = amount / divisor; diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 1f45d5241..706ee9684 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -6,15 +6,14 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from '../../../environments/environment'; import { AssetsService } from '../../services/assets.service'; -import { filter, map, tap, switchMap, shareReplay, catchError } from 'rxjs/operators'; +import { filter, map, tap, switchMap, catchError } from 'rxjs/operators'; import { BlockExtended } from '../../interfaces/node-api.interface'; import { ApiService } from '../../services/api.service'; import { PriceService } from '../../services/price.service'; import { StorageService } from '../../services/storage.service'; import { OrdApiService } from '../../services/ord-api.service'; import { Inscription } from '../ord-data/ord-data.component'; -import { Runestone } from '../../shared/ord/rune/runestone'; -import { Etching } from '../../shared/ord/rune/etching'; +import { Etching, Runestone } from '../../shared/ord/rune.utils'; @Component({ selector: 'app-transactions-list', @@ -261,7 +260,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx.vout[i].isRunestone = true; break; } - } + } } }); diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts index bc726e839..da75a74af 100644 --- a/frontend/src/app/services/ord-api.service.ts +++ b/frontend/src/app/services/ord-api.service.ts @@ -3,10 +3,9 @@ import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs' import { Inscription } from '../components/ord-data/ord-data.component'; import { Transaction } from '../interfaces/electrs.interface'; import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils'; -import { Runestone } from '../shared/ord/rune/runestone'; -import { Etching } from '../shared/ord/rune/etching'; +import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '../shared/ord/rune.utils'; import { ElectrsApiService } from './electrs-api.service'; -import { UNCOMMON_GOODS } from '../shared/ord/rune/runestone'; + @Injectable({ providedIn: 'root' @@ -18,27 +17,16 @@ export class OrdApiService { ) { } decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { - const runestoneTx = { vout: tx.vout.map(vout => ({ scriptpubkey: vout.scriptpubkey })) }; - const decipher = Runestone.decipher(runestoneTx); - - // For now, ignore cenotaphs - let message = decipher.isSome() ? decipher.unwrap() : null; - if (message?.type === 'cenotaph') { - return of({ runestone: null, runeInfo: {} }); - } - - const runestone = message as Runestone; + const runestone = decipherRunestone(tx); const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; const runesToFetch: Set = new Set(); if (runestone) { - if (runestone.mint.isSome()) { - const mint = runestone.mint.unwrap().toString(); - - if (mint === '1:0') { - runeInfo[mint] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; + if (runestone.mint) { + if (runestone.mint.toString() === '1:0') { + runeInfo[runestone.mint.toString()] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; } else { - runesToFetch.add(mint); + runesToFetch.add(runestone.mint.toString()); } } @@ -65,9 +53,10 @@ export class OrdApiService { }) ); } + return of({ runestone: runestone, runeInfo }); + } else { + return of({ runestone: null, runeInfo: {} }); } - - return of({ runestone: runestone, runeInfo }); } // Get etching from runeId by looking up the transaction that etched the rune @@ -78,11 +67,11 @@ export class OrdApiService { switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), switchMap(txId => this.electrsApiService.getTransaction$(txId)), switchMap(tx => { - const decipheredMessage = Runestone.decipher(tx); - if (decipheredMessage.isSome()) { - const message = decipheredMessage.unwrap(); - if (message?.type === 'runestone' && message.etching.isSome()) { - return of({ etching: message.etching.unwrap(), txid: tx.txid }); + const runestone = decipherRunestone(tx); + if (runestone) { + const etching = runestone.etching; + if (etching) { + return of({ etching, txid: tx.txid }); } } return of(null); diff --git a/frontend/src/app/shared/ord/rune.utils.ts b/frontend/src/app/shared/ord/rune.utils.ts new file mode 100644 index 000000000..a1f947b46 --- /dev/null +++ b/frontend/src/app/shared/ord/rune.utils.ts @@ -0,0 +1,258 @@ +import { Transaction } from '../../interfaces/electrs.interface'; + +export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; + +export class RuneId { + block: number; + index: number; + + constructor(block: number, index: number) { + this.block = block; + this.index = index; + } + + toString(): string { + return `${this.block}:${this.index}`; + } +} + +export type Etching = { + divisibility?: number; + premine?: bigint; + symbol?: string; + terms?: { + cap?: bigint; + amount?: bigint; + offset?: { + start?: bigint; + end?: bigint; + }; + height?: { + start?: bigint; + end?: bigint; + }; + }; + turbo?: boolean; + name?: string; + spacedName?: string; + supply?: bigint; +}; + +export type Edict = { + id: RuneId; + amount: bigint; + output: number; +}; + +export type Runestone = { + mint?: RuneId; + pointer?: number; + edicts?: Edict[]; + etching?: Etching; +}; + +type Message = { + fields: Record; + edicts: Edict[]; +} + +export const UNCOMMON_GOODS: Etching = { + divisibility: 0, + premine: 0n, + symbol: '⧉', + terms: { + cap: U128_MAX_BIGINT, + amount: 1n, + offset: { + start: 0n, + end: 0n, + }, + height: { + start: 840000n, + end: 1050000n, + }, + }, + turbo: false, + name: 'UNCOMMONGOODS', + spacedName: 'UNCOMMON•GOODS', + supply: U128_MAX_BIGINT, +}; + +enum Tag { + Body = 0, + Flags = 2, + Rune = 4, + Premine = 6, + Cap = 8, + Amount = 10, + HeightStart = 12, + HeightEnd = 14, + OffsetStart = 16, + OffsetEnd = 18, + Mint = 20, + Pointer = 22, + Cenotaph = 126, + + Divisibility = 1, + Spacers = 3, + Symbol = 5, + Nop = 127, +} + +const Flag = { + ETCHING: 1n, + TERMS: 1n << 1n, + TURBO: 1n << 2n, + CENOTAPH: 1n << 127n, +}; + +function hexToBytes(hex: string): Uint8Array { + return new Uint8Array(hex.match(/.{2}/g).map((byte) => parseInt(byte, 16))); +} + +function decodeLEB128(bytes: Uint8Array): bigint[] { + const integers: bigint[] = []; + let index = 0; + while (index < bytes.length) { + let value = BigInt(0); + let shift = 0; + let byte: number; + do { + byte = bytes[index++]; + value |= BigInt(byte & 0x7f) << BigInt(shift); + shift += 7; + } while (byte & 0x80); + integers.push(value); + } + return integers; +} + +function integersToMessage(integers: bigint[]): Message { + const message = { + fields: {}, + edicts: [], + }; + let inBody = false; + while (integers.length) { + if (!inBody) { + // The integers are interpreted as a sequence of tag/value pairs, with duplicate tags appending their value to the field value. + const tag: Tag = Number(integers.shift()); + if (tag === Tag.Body) { + inBody = true; + } else { + const value = integers.shift(); + if (message.fields[tag]) { + message.fields[tag].push(value); + } else { + message.fields[tag] = [value]; + } + } + } else { + // If a tag with value zero is encountered, all following integers are interpreted as a series of four-integer edicts, each consisting of a rune ID block height, rune ID transaction index, amount, and output. + const height = integers.shift(); + const txIndex = integers.shift(); + const amount = integers.shift(); + const output = integers.shift(); + message.edicts.push({ + id: { + block: height, + index: txIndex, + }, + amount, + output, + }); + } + } + return message; +} + +function parseRuneName(rune: bigint): string { + let name = ''; + rune += 1n; + while (rune > 0n) { + name = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((rune - 1n) % 26n)] + name; + rune = (rune - 1n) / 26n; + } + return name; +} + +function spaceRuneName(name: string, spacers: bigint): string { + let i = 0; + let spacedName = ''; + while (spacers > 0n || i < name.length) { + spacedName += name[i]; + if (spacers & 1n) { + spacedName += '•'; + } + if (spacers > 0n) { + spacers >>= 1n; + } + i++; + } + return spacedName; +} + +function messageToRunestone(message: Message): Runestone { + let etching: Etching | undefined; + let mint: RuneId | undefined; + let pointer: number | undefined; + + const flags = message.fields[Tag.Flags]?.[0] || 0n; + if (flags & Flag.ETCHING) { + const hasTerms = (flags & Flag.TERMS) > 0n; + const isTurbo = (flags & Flag.TURBO) > 0n; + const name = parseRuneName(message.fields[Tag.Rune][0]); + etching = { + divisibility: Number(message.fields[Tag.Divisibility][0]), + premine: message.fields[Tag.Premine]?.[0], + symbol: message.fields[Tag.Symbol]?.[0] ? String.fromCodePoint(Number(message.fields[Tag.Symbol][0])) : '¤', + terms: hasTerms ? { + cap: message.fields[Tag.Cap]?.[0], + amount: message.fields[Tag.Amount]?.[0], + offset: { + start: message.fields[Tag.OffsetStart]?.[0], + end: message.fields[Tag.OffsetEnd]?.[0], + }, + height: { + start: message.fields[Tag.HeightStart]?.[0], + end: message.fields[Tag.HeightEnd]?.[0], + }, + } : undefined, + turbo: isTurbo, + name, + spacedName: spaceRuneName(name, message.fields[Tag.Spacers]?.[0] ?? 0n), + }; + etching.supply = ( + (etching.terms?.cap ?? 0n) * (etching.terms?.amount ?? 0n) + ) + (etching.premine ?? 0n); + } + const mintField = message.fields[Tag.Mint]; + if (mintField) { + mint = new RuneId(Number(mintField[0]), Number(mintField[1])); + } + const pointerField = message.fields[Tag.Pointer]; + if (pointerField) { + pointer = Number(pointerField[0]); + } + return { + mint, + pointer, + edicts: message.edicts, + etching, + }; +} + +export function decipherRunestone(tx: Transaction): Runestone | void { + const payload = tx.vout.find((vout) => vout.scriptpubkey.startsWith('6a5d'))?.scriptpubkey_asm.replace(/OP_\w+|\s/g, ''); + if (!payload) { + return; + } + try { + const integers = decodeLEB128(hexToBytes(payload)); + const message = integersToMessage(integers); + return messageToRunestone(message); + } catch (error) { + console.error(error); + return; + } +} diff --git a/frontend/src/app/shared/ord/rune/artifact.ts b/frontend/src/app/shared/ord/rune/artifact.ts deleted file mode 100644 index 2eba9f158..000000000 --- a/frontend/src/app/shared/ord/rune/artifact.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Cenotaph } from './cenotaph'; -import { Runestone } from './runestone'; - -export type Artifact = Cenotaph | Runestone; diff --git a/frontend/src/app/shared/ord/rune/cenotaph.ts b/frontend/src/app/shared/ord/rune/cenotaph.ts deleted file mode 100644 index 368a0f938..000000000 --- a/frontend/src/app/shared/ord/rune/cenotaph.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Flaw } from './flaw'; -import { None, Option } from './monads'; -import { Rune } from './rune'; -import { RuneId } from './runeid'; - -export class Cenotaph { - readonly type = 'cenotaph'; - - constructor( - readonly flaws: Flaw[], - readonly etching: Option = None, - readonly mint: Option = None - ) {} -} diff --git a/frontend/src/app/shared/ord/rune/constants.ts b/frontend/src/app/shared/ord/rune/constants.ts deleted file mode 100644 index 0e4bab116..000000000 --- a/frontend/src/app/shared/ord/rune/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { u8 } from './integer'; -import { opcodes } from './script'; - -export const MAX_DIVISIBILITY = u8(38); - -export const OP_RETURN = opcodes.OP_RETURN; -export const MAGIC_NUMBER = opcodes.OP_13; diff --git a/frontend/src/app/shared/ord/rune/edict.ts b/frontend/src/app/shared/ord/rune/edict.ts deleted file mode 100644 index ede5865a6..000000000 --- a/frontend/src/app/shared/ord/rune/edict.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Option, Some, None } from './monads'; -import { RuneId } from './runeid'; -import { u128, u32 } from './integer'; - -export type Edict = { - id: RuneId; - amount: u128; - output: u32; -}; - -export namespace Edict { - export function fromIntegers( - numOutputs: number, - id: RuneId, - amount: u128, - output: u128 - ): Option { - if (id.block === 0n && id.tx > 0n) { - return None; - } - - const optionOutputU32 = u128.tryIntoU32(output); - if (optionOutputU32.isNone()) { - return None; - } - const outputU32 = optionOutputU32.unwrap(); - - if (outputU32 > numOutputs) { - return None; - } - - return Some({ id, amount, output: outputU32 }); - } -} diff --git a/frontend/src/app/shared/ord/rune/etching.ts b/frontend/src/app/shared/ord/rune/etching.ts deleted file mode 100644 index edc245565..000000000 --- a/frontend/src/app/shared/ord/rune/etching.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { None, Option, Some } from './monads'; -import { Terms } from './terms'; -import { Rune } from './rune'; -import { u128, u32, u8 } from './integer'; - -type RuneEtchingBase = { - divisibility?: number; - premine?: bigint; - symbol?: string; - terms?: { - cap?: bigint; - amount?: bigint; - offset?: { - start?: bigint; - end?: bigint; - }; - height?: { - start?: bigint; - end?: bigint; - }; - }; - turbo?: boolean; -}; - -export type RuneEtchingSpec = RuneEtchingBase & { runeName?: string }; - -export class Etching { - readonly symbol: Option; - - constructor( - readonly divisibility: Option, - readonly rune: Option, - readonly spacers: Option, - symbol: Option, - readonly terms: Option, - readonly premine: Option, - readonly turbo: boolean - ) { - this.symbol = symbol.andThen((value) => { - const codePoint = value.codePointAt(0); - return codePoint !== undefined ? Some(String.fromCodePoint(codePoint)) : None; - }); - } - - get supply(): Option { - const premine = this.premine.unwrapOr(u128(0)); - const cap = this.terms.andThen((terms) => terms.cap).unwrapOr(u128(0)); - const amount = this.terms.andThen((terms) => terms.amount).unwrapOr(u128(0)); - - return u128 - .checkedMultiply(cap, amount) - .andThen((multiplyResult) => u128.checkedAdd(premine, multiplyResult)); - } -} diff --git a/frontend/src/app/shared/ord/rune/flag.ts b/frontend/src/app/shared/ord/rune/flag.ts deleted file mode 100644 index 317c74ae5..000000000 --- a/frontend/src/app/shared/ord/rune/flag.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { u128 } from './integer'; - -export enum Flag { - ETCHING = 0, - TERMS = 1, - TURBO = 2, - CENOTAPH = 127, -} - -export namespace Flag { - export function mask(flag: Flag): u128 { - return u128(1n << BigInt(flag)); - } - - export function take(flags: u128, flag: Flag): { set: boolean; flags: u128 } { - const mask = Flag.mask(flag); - const set = (flags & mask) !== 0n; - return { set, flags: set ? u128(flags - mask) : flags }; - } -} diff --git a/frontend/src/app/shared/ord/rune/flaw.ts b/frontend/src/app/shared/ord/rune/flaw.ts deleted file mode 100644 index 2ed5ea506..000000000 --- a/frontend/src/app/shared/ord/rune/flaw.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum Flaw { - EDICT_OUTPUT, - EDICT_RUNE_ID, - INVALID_SCRIPT, - OPCODE, - SUPPLY_OVERFLOW, - TRAILING_INTEGERS, - TRUNCATED_FIELD, - UNRECOGNIZED_EVEN_TAG, - UNRECOGNIZED_FLAG, - VARINT, -} diff --git a/frontend/src/app/shared/ord/rune/integer/index.ts b/frontend/src/app/shared/ord/rune/integer/index.ts deleted file mode 100644 index 3c54a77e7..000000000 --- a/frontend/src/app/shared/ord/rune/integer/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { u8 } from './u8'; -export { u32 } from './u32'; -export { u64 } from './u64'; -export { u128 } from './u128'; diff --git a/frontend/src/app/shared/ord/rune/integer/u128.ts b/frontend/src/app/shared/ord/rune/integer/u128.ts deleted file mode 100644 index 78de8506f..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u128.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { None, Option, Some } from '../monads'; -import { SeekArray } from '../seekarray'; -import { u64 } from './u64'; -import { u32 } from './u32'; -import { u8 } from './u8'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -/** - * ## 128-bit unsigned integer - * - * - **Value Range:** `0` to `340282366920938463463374607431768211455` - * - **Size in bytes:** `16` - * - **Web IDL type:** `bigint` - * - **Equivalent C type:** `uint128_t` - */ -export type u128 = BigTypedNumber<'u128'>; - -export const U128_MAX_BIGINT = 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffffn; - -/** - * Convert Number or BigInt to 128-bit unsigned integer. - * @param num - The Number or BigInt to convert. - * @returns - The resulting 128-bit unsigned integer (BigInt). - */ -export function u128(num: number | bigint): u128 { - if (typeof num == 'bigint') { - if (num < 0n || num > U128_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u128; -} - -export namespace u128 { - export const MAX = u128(U128_MAX_BIGINT); - - export function checkedAdd(x: u128, y: u128): Option { - const result = x + y; - if (result > u128.MAX) { - return None; - } - - return Some(u128(result)); - } - - export function checkedAddThrow(x: u128, y: u128): u128 { - const option = u128.checkedAdd(x, y); - if (option.isNone()) { - throw new Error('checked add overflow'); - } - return option.unwrap(); - } - - export function checkedSub(x: u128, y: u128): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u128(result)); - } - - export function checkedSubThrow(x: u128, y: u128): u128 { - const option = u128.checkedSub(x, y); - if (option.isNone()) { - throw new Error('checked sub overflow'); - } - return option.unwrap(); - } - - export function checkedMultiply(x: u128, y: u128): Option { - const result = x * y; - if (result > u128.MAX) { - return None; - } - - return Some(u128(result)); - } - - export function saturatingAdd(x: u128, y: u128): u128 { - const result = x + y; - return result > u128.MAX ? u128.MAX : u128(result); - } - - export function saturatingMultiply(x: u128, y: u128): u128 { - const result = x * y; - return result > u128.MAX ? u128.MAX : u128(result); - } - - export function saturatingSub(x: u128, y: u128): u128 { - return u128(x < y ? 0 : x - y); - } - - export function decodeVarInt(seekArray: SeekArray): Option { - try { - return Some(tryDecodeVarInt(seekArray)); - } catch (e) { - return None; - } - } - - export function tryDecodeVarInt(seekArray: SeekArray): u128 { - let result: u128 = u128(0); - for (let i = 0; i <= 18; i++) { - const byte = seekArray.readUInt8(); - if (byte === undefined) throw new Error('Unterminated or invalid data'); - - // Ensure all operations are done in bigint domain. - const byteBigint = BigInt(byte); - const value = u128(byteBigint & 0x7Fn); // Ensure the 'value' is treated as u128. - - if (i === 18 && (value & 0x7Cn) !== 0n) throw new Error('Overflow'); - - // Use bigint addition instead of bitwise OR to combine the results, - // and ensure shifting is handled correctly within the bigint domain. - result = u128(result + (value << (7n * BigInt(i)))); - - if ((byte & 0x80) === 0) return result; - } - throw new Error('Overlong encoding'); - } - - export function encodeVarInt(value: u128): Uint8Array { - const bytes = []; - while (value >> 7n > 0n) { - bytes.push(Number(value & 0x7Fn) | 0x80); - value = u128(value >> 7n); // Explicitly cast the shifted value back to u128 - } - bytes.push(Number(value & 0x7Fn)); - return new Uint8Array(bytes); - } - - export function tryIntoU64(n: u128): Option { - return n > u64.MAX ? None : Some(u64(n)); - } - - export function tryIntoU32(n: u128): Option { - return n > u32.MAX ? None : Some(u32(n)); - } - - export function tryIntoU8(n: u128): Option { - return n > u8.MAX ? None : Some(u8(n)); - } -} - -export function* getAllU128(data: Uint8Array): Generator { - const seekArray = new SeekArray(data); - while (!seekArray.isFinished()) { - const nextValue = u128.decodeVarInt(seekArray); - if (nextValue.isNone()) { - return; - } - yield nextValue.unwrap(); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u32.ts b/frontend/src/app/shared/ord/rune/integer/u32.ts deleted file mode 100644 index 90e517bb8..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u32.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u32 = BigTypedNumber<'u32'>; - -export const U32_MAX_BIGINT = 0xffff_ffffn; - -export function u32(num: number | bigint): u32 { - if (typeof num == 'bigint') { - if (num < 0n || num > U32_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u32; -} - -export namespace u32 { - export const MAX = u32(U32_MAX_BIGINT); - - export function checkedAdd(x: u32, y: u32): Option { - const result = x + y; - if (result > u32.MAX) { - return None; - } - - return Some(u32(result)); - } - - export function checkedSub(x: u32, y: u32): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u32(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u64.ts b/frontend/src/app/shared/ord/rune/integer/u64.ts deleted file mode 100644 index 8010dd99c..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u64.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u64 = BigTypedNumber<'u64'>; - -export const U64_MAX_BIGINT = 0xffff_ffff_ffff_ffffn; - -export function u64(num: number | bigint): u64 { - if (typeof num == 'bigint') { - if (num < 0n || num > U64_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u64; -} - -export namespace u64 { - export const MAX = u64(U64_MAX_BIGINT); - - export function checkedAdd(x: u64, y: u64): Option { - const result = x + y; - if (result > u64.MAX) { - return None; - } - - return Some(u64(result)); - } - - export function checkedSub(x: u64, y: u64): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u64(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/integer/u8.ts b/frontend/src/app/shared/ord/rune/integer/u8.ts deleted file mode 100644 index 5676421b0..000000000 --- a/frontend/src/app/shared/ord/rune/integer/u8.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { None, Option, Some } from '../monads'; - -/** - * A little utility type used for nominal typing. - * - * See {@link https://michalzalecki.com/nominal-typing-in-typescript/} - */ -type BigTypedNumber = bigint & { - /** - * # !!! DO NOT USE THIS PROPERTY IN YOUR CODE !!! - * ## This is just used to make each `BigTypedNumber` alias unique for Typescript and doesn't actually exist. - * @ignore - * @private - * @readonly - * @type {undefined} - */ - readonly __kind__: T; -}; - -export type u8 = BigTypedNumber<'u8'>; - -export const U8_MAX_BIGINT = 0xffn; - -export function u8(num: number | bigint): u8 { - if (typeof num == 'bigint') { - if (num < 0n || num > U8_MAX_BIGINT) { - throw new Error('num is out of range'); - } - } else { - if (!Number.isSafeInteger(num) || num < 0) { - throw new Error('num is not a valid integer'); - } - } - - return BigInt(num) as u8; -} - -export namespace u8 { - export const MAX = u8(U8_MAX_BIGINT); - - export function checkedAdd(x: u8, y: u8): Option { - const result = x + y; - if (result > u8.MAX) { - return None; - } - - return Some(u8(result)); - } - - export function checkedSub(x: u8, y: u8): Option { - const result = x - y; - if (result < 0n) { - return None; - } - - return Some(u8(result)); - } -} diff --git a/frontend/src/app/shared/ord/rune/message.ts b/frontend/src/app/shared/ord/rune/message.ts deleted file mode 100644 index cad1a8ced..000000000 --- a/frontend/src/app/shared/ord/rune/message.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Edict } from './edict'; -import { Flaw } from './flaw'; -import { u128, u64, u32 } from './integer'; -import { RuneId } from './runeid'; -import { Tag } from './tag'; - -export class Message { - constructor( - readonly flaws: Flaw[], - readonly edicts: Edict[], - readonly fields: Map - ) {} - - static fromIntegers(numOutputs: number, payload: u128[]): Message { - const edicts: Edict[] = []; - const fields = new Map(); - const flaws: Flaw[] = []; - - for (const i of [...Array(Math.ceil(payload.length / 2)).keys()].map((n) => n * 2)) { - const tag = payload[i]; - - if (u128(Tag.BODY) === tag) { - let id = new RuneId(u64(0), u32(0)); - const chunkSize = 4; - - const body = payload.slice(i + 1); - for (let j = 0; j < body.length; j += chunkSize) { - const chunk = body.slice(j, j + chunkSize); - if (chunk.length !== chunkSize) { - flaws.push(Flaw.TRAILING_INTEGERS); - break; - } - - const optionNext = id.next(chunk[0], chunk[1]); - if (optionNext.isNone()) { - flaws.push(Flaw.EDICT_RUNE_ID); - break; - } - const next = optionNext.unwrap(); - - const optionEdict = Edict.fromIntegers(numOutputs, next, chunk[2], chunk[3]); - if (optionEdict.isNone()) { - flaws.push(Flaw.EDICT_OUTPUT); - break; - } - const edict = optionEdict.unwrap(); - - id = next; - edicts.push(edict); - } - break; - } - - const value = payload[i + 1]; - if (value === undefined) { - flaws.push(Flaw.TRUNCATED_FIELD); - break; - } - - const values = fields.get(tag) ?? []; - values.push(value); - fields.set(tag, values); - } - - return new Message(flaws, edicts, fields); - } -} diff --git a/frontend/src/app/shared/ord/rune/monads.ts b/frontend/src/app/shared/ord/rune/monads.ts deleted file mode 100644 index 7822acca9..000000000 --- a/frontend/src/app/shared/ord/rune/monads.ts +++ /dev/null @@ -1,392 +0,0 @@ -// Copied with MIT License from link below: -// https://github.com/thames-technology/monads/blob/de957d3d68449d659518d99be4ea74bbb70dfc8e/src/option/option.ts - -/** - * Type representing any value except 'undefined'. - * This is useful when working with strict null checks, ensuring that a value can be null but not undefined. - */ -type NonUndefined = {} | null; // eslint-disable-line @typescript-eslint/ban-types - -/** - * Enum-like object to represent the type of an Option (Some or None). - */ -export const OptionType = { - Some: Symbol(':some'), - None: Symbol(':none'), -}; - -/** - * Interface for handling match operations on an Option. - * Allows executing different logic based on the Option being Some or None. - */ -interface Match { - some: (val: A) => B; - none: (() => B) | B; -} - -/** - * The Option interface representing an optional value. - * An Option is either Some, holding a value, or None, indicating the absence of a value. - */ -export interface Option { - /** - * Represents the type of the Option: either Some or None. Useful for debugging and runtime checks. - */ - type: symbol; - - /** - * Determines if the Option is a Some. - * - * @returns true if the Option is Some, otherwise false. - * - * #### Example - * - * ```ts - * console.log(Some(5).isSome()); // true - * console.log(None.isSome()); // false - * ``` - */ - isSome(): boolean; - - /** - * Determines if the Option is None. - * - * @returns true if the Option is None, otherwise false. - * - * #### Example - * - * ```ts - * console.log(Some(5).isNone()); // false - * console.log(None.isNone()); // true - * ``` - */ - isNone(): boolean; - - /** - * Performs a match operation on the Option, allowing for branching logic based on its state. - * This method takes an object with functions for each case (Some or None) and executes - * the corresponding function based on the Option's state, returning the result. - * - * @param fn An object containing two properties: `some` and `none`, which are functions - * to handle the Some and None cases, respectively. - * @returns The result of applying the corresponding function based on the Option's state. - * - * #### Example - * - * ```ts - * const optionSome = Some(5); - * const matchResultSome = optionSome.match({ - * some: (value) => `The value is ${value}.`, - * none: () => 'There is no value.', - * }); - * console.log(matchResultSome); // Outputs: "The value is 5." - * - * const optionNone = None; - * const matchResultNone = optionNone.match({ - * some: (value) => `The value is ${value}.`, - * none: () => 'There is no value.', - * }); - * console.log(matchResultNone); // Outputs: "There is no value." - * ``` - */ - match(fn: Match): U; - - /** - * Applies a function to the contained value (if any), or returns a default if None. - * - * @param fn A function that takes a value of type T and returns a value of type U. - * @returns An Option containing the function's return value if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const length = Some("hello").map(s => s.length); // Some(5) - * const noneLength = None.map(s => s.length); // None - * ``` - */ - map(fn: (val: T) => U): Option; - - inspect(fn: (val: T) => void): Option; - - /** - * Transforms the Option into another by applying a function to the contained value, - * chaining multiple potentially failing operations. - * - * @param fn A function that takes a value of type T and returns an Option of type U. - * @returns The Option returned by the function if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const parse = (s: string) => { - * const parsed = parseInt(s); - * return isNaN(parsed) ? None : Some(parsed); - * }; - * const result = Some("123").andThen(parse); // Some(123) - * const noResult = Some("abc").andThen(parse); // None - * ``` - */ - andThen(fn: (val: T) => Option): Option; - - /** - * Returns this Option if it is Some, otherwise returns the option provided as a parameter. - * - * @param optb The alternative Option to return if the original Option is None. - * @returns The original Option if it is Some, otherwise `optb`. - * - * #### Examples - * - * ```ts - * const defaultOption = Some("default"); - * const someOption = Some("some").or(defaultOption); // Some("some") - * const noneOption = None.or(defaultOption); // Some("default") - * ``` - */ - or(optb: Option): Option; - - orElse(optb: () => Option): Option; - - /** - * Returns the option provided as a parameter if the original Option is Some, otherwise returns None. - * - * @param optb The Option to return if the original Option is Some. - * @returns `optb` if the original Option is Some, otherwise None. - * - * #### Examples - * - * ```ts - * const anotherOption = Some("another"); - * const someOption = Some("some").and(anotherOption); // Some("another") - * const noneOption = None.and(anotherOption); // None - * ``` - */ - and(optb: Option): Option; - - /** - * Returns the contained value if Some, otherwise returns the provided default value. - * - * @param def The default value to return if the Option is None. - * @returns The contained value if Some, otherwise `def`. - * - * #### Examples - * - * ```ts - * const someValue = Some("value").unwrapOr("default"); // "value" - * const noneValue = None.unwrapOr("default"); // "default" - * ``` - */ - unwrapOr(def: T): T; - - /** - * Unwraps an Option, yielding the contained value if Some, otherwise throws an error. - * - * @returns The contained value. - * @throws Error if the Option is None. - * - * #### Examples - * - * ```ts - * console.log(Some("value").unwrap()); // "value" - * console.log(None.unwrap()); // throws Error - * ``` - */ - unwrap(): T | never; -} - -/** - * Implementation of Option representing a value (Some). - */ -interface SomeOption extends Option { - unwrap(): T; -} - -/** - * Implementation of Option representing the absence of a value (None). - */ -interface NoneOption extends Option { - unwrap(): never; -} - -/** - * Represents a Some value of Option. - */ -class SomeImpl implements SomeOption { - constructor(private readonly val: T) {} - - get type() { - return OptionType.Some; - } - - isSome() { - return true; - } - - isNone() { - return false; - } - - match(fn: Match): B { - return fn.some(this.val); - } - - map(fn: (val: T) => U): Option { - return Some(fn(this.val)); - } - - inspect(fn: (val: T) => void): Option { - fn(this.val); - return this; - } - - andThen(fn: (val: T) => Option): Option { - return fn(this.val); - } - - or(_optb: Option): Option { - return this; - } - - orElse(optb: () => Option): Option { - return this; - } - - and(optb: Option): Option { - return optb; - } - - unwrapOr(_def: T): T { - return this.val; - } - - unwrap(): T { - return this.val; - } -} - -/** - * Represents a None value of Option. - */ -class NoneImpl implements NoneOption { - get type() { - return OptionType.None; - } - - isSome() { - return false; - } - - isNone() { - return true; - } - - match({ none }: Match): U { - if (typeof none === 'function') { - return (none as () => U)(); - } - - return none; - } - - map(_fn: (val: T) => U): Option { - return new NoneImpl(); - } - - inspect(fn: (val: T) => void): Option { - return this; - } - - andThen(_fn: (val: T) => Option): Option { - return new NoneImpl(); - } - - or(optb: Option): Option { - return optb; - } - - orElse(optb: () => Option): Option { - return optb(); - } - - and(_optb: Option): Option { - return new NoneImpl(); - } - - unwrapOr(def: T): T { - return def; - } - - unwrap(): never { - throw new ReferenceError('Trying to unwrap None.'); - } -} - -/** - * Creates a Some instance of Option containing the given value. - * This function is used to represent the presence of a value in an operation that may not always produce a value. - * - * @param val The value to be wrapped in a Some Option. - * @returns An Option instance representing the presence of a value. - * - * #### Example - * - * ```ts - * const option = Some(42); - * console.log(option.unwrap()); // Outputs: 42 - * ``` - */ -export function Some(val: T): Option { - return new SomeImpl(val); -} - -/** - * The singleton instance representing None, an Option with no value. - * This constant is used to represent the absence of a value in operations that may not always produce a value. - * - * #### Example - * - * ```ts - * const option = None; - * console.log(option.isNone()); // Outputs: true - * ``` - */ -export const None: Option = new NoneImpl(); // eslint-disable-line @typescript-eslint/no-explicit-any - -/** - * Type guard to check if an Option is a Some value. - * This function is used to narrow down the type of an Option to SomeOption in TypeScript's type system. - * - * @param val The Option to be checked. - * @returns true if the provided Option is a SomeOption, false otherwise. - * - * #### Example - * - * ```ts - * const option = Some('Success'); - * if (isSome(option)) { - * console.log('Option has a value:', option.unwrap()); - * } - * ``` - */ -export function isSome(val: Option): val is SomeOption { - return val.isSome(); -} - -/** - * Type guard to check if an Option is a None value. - * This function is used to narrow down the type of an Option to NoneOption in TypeScript's type system. - * - * @param val The Option to be checked. - * @returns true if the provided Option is a NoneOption, false otherwise. - * - * #### Example - * - * ```ts - * const option = None; - * if (isNone(option)) { - * console.log('Option does not have a value.'); - * } - * ``` - */ -export function isNone(val: Option): val is NoneOption { - return val.isNone(); -} diff --git a/frontend/src/app/shared/ord/rune/rune.ts b/frontend/src/app/shared/ord/rune/rune.ts deleted file mode 100644 index c0dd96e1b..000000000 --- a/frontend/src/app/shared/ord/rune/rune.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { u128 } from './integer'; - -export class Rune { - - constructor(readonly value: u128) {} - - toString() { - let n = this.value; - - if (n === u128.MAX) { - return 'BCGDENLQRQWDSLRUGSNLBTMFIJAV'; - } - - n = u128(n + 1n); - let symbol = ''; - while (n > 0) { - symbol = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Number((n - 1n) % 26n)] + symbol; - n = u128((n - 1n) / 26n); - } - - return symbol; - } -} diff --git a/frontend/src/app/shared/ord/rune/runeid.ts b/frontend/src/app/shared/ord/rune/runeid.ts deleted file mode 100644 index ca0e938b7..000000000 --- a/frontend/src/app/shared/ord/rune/runeid.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { None, Option, Some } from './monads'; -import { u64, u32, u128 } from './integer'; - -export class RuneId { - constructor(readonly block: u64, readonly tx: u32) {} - - static new(block: u64, tx: u32): Option { - const id = new RuneId(block, tx); - - if (id.block === 0n && id.tx > 0) { - return None; - } - - return Some(id); - } - - static sort(runeIds: RuneId[]): RuneId[] { - return [...runeIds].sort((x, y) => Number(x.block - y.block || x.tx - y.tx)); - } - - delta(next: RuneId): Option<[u128, u128]> { - const optionBlock = u64.checkedSub(next.block, this.block); - if (optionBlock.isNone()) { - return None; - } - const block = optionBlock.unwrap(); - - let tx: u32; - if (block === 0n) { - const optionTx = u32.checkedSub(next.tx, this.tx); - if (optionTx.isNone()) { - return None; - } - tx = optionTx.unwrap(); - } else { - tx = next.tx; - } - - return Some([u128(block), u128(tx)]); - } - - next(block: u128, tx: u128): Option { - const optionBlock = u128.tryIntoU64(block); - const optionTx = u128.tryIntoU32(tx); - - if (optionBlock.isNone() || optionTx.isNone()) { - return None; - } - - const blockU64 = optionBlock.unwrap(); - const txU32 = optionTx.unwrap(); - - const nextBlock = u64.checkedAdd(this.block, blockU64); - if (nextBlock.isNone()) { - return None; - } - - let nextTx: u32; - if (blockU64 === 0n) { - const optionAdd = u32.checkedAdd(this.tx, txU32); - if (optionAdd.isNone()) { - return None; - } - - nextTx = optionAdd.unwrap(); - } else { - nextTx = txU32; - } - - return RuneId.new(nextBlock.unwrap(), nextTx); - } - - toString() { - return `${this.block}:${this.tx}`; - } - - static fromString(s: string) { - const parts = s.split(':'); - if (parts.length !== 2) { - throw new Error(`invalid rune ID: ${s}`); - } - - const [block, tx] = parts; - if (!/^\d+$/.test(block) || !/^\d+$/.test(tx)) { - throw new Error(`invalid rune ID: ${s}`); - } - return new RuneId(u64(BigInt(block)), u32(BigInt(tx))); - } -} diff --git a/frontend/src/app/shared/ord/rune/runestone.ts b/frontend/src/app/shared/ord/rune/runestone.ts deleted file mode 100644 index c71cdcd90..000000000 --- a/frontend/src/app/shared/ord/rune/runestone.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { concatUint8Arrays, hexToBytes } from '../inscription.utils'; -import { Artifact } from './artifact'; -import { Cenotaph } from './cenotaph'; -import { MAGIC_NUMBER, MAX_DIVISIBILITY, OP_RETURN } from './constants'; -import { Edict } from './edict'; -import { Etching } from './etching'; -import { Flag } from './flag'; -import { Flaw } from './flaw'; -import { u128, u32, u64, u8 } from './integer'; -import { Message } from './message'; -import { None, Option, Some } from './monads'; -import { Rune } from './rune'; -import { RuneId } from './runeid'; -import { script } from './script'; -import { SeekArray } from './seekarray'; -import { Tag } from './tag'; - -export const MAX_SPACERS = 0b00000111_11111111_11111111_11111111; - -export const UNCOMMON_GOODS = new Etching( - Some(u8(0)), - Some(new Rune(u128(2055900680524219742n))), - Some(u32(128)), - Some('⧉'), - Some({ - amount: Some(u128(1)), - cap: Some(u128(340282366920938463463374607431768211455n)), - height: [Some(u64(840000)), Some(u64(1050000))], - offset: [Some(u64(0)), Some(u64(0))], - }), - Some(u128(0)), - false -); - -// New: Esplora format instead of Bitcoin RPC format -export type RunestoneTx = { - vout: { - scriptpubkey: string - }[]; -}; - -type Payload = Uint8Array | Flaw; - -export class Runestone { - readonly type = 'runestone'; - - constructor( - readonly mint: Option, - readonly pointer: Option, - readonly edicts: Edict[], - readonly etching: Option - ) {} - - static decipher(transaction: RunestoneTx): Option { - const optionPayload = Runestone.payload(transaction); - if (optionPayload.isNone()) { - return None; - } - const payload = optionPayload.unwrap(); - if (!(payload instanceof Uint8Array)) { - return Some(new Cenotaph([payload])); - } - - const optionIntegers = Runestone.integers(payload); - if (optionIntegers.isNone()) { - return Some(new Cenotaph([Flaw.VARINT])); - } - - const { flaws, edicts, fields } = Message.fromIntegers( - transaction.vout.length, - optionIntegers.unwrap() - ); - - let flags = Tag.take(Tag.FLAGS, fields, 1, ([value]) => Some(value)).unwrapOr(u128(0)); - - const etchingResult = Flag.take(flags, Flag.ETCHING); - const etchingFlag = etchingResult.set; - flags = etchingResult.flags; - - const etching: Option = etchingFlag - ? (() => { - const divisibility = Tag.take( - Tag.DIVISIBILITY, - fields, - 1, - ([value]): Option => - u128 - .tryIntoU8(value) - .andThen((value) => (value <= MAX_DIVISIBILITY ? Some(value) : None)) - ); - - const rune = Tag.take(Tag.RUNE, fields, 1, ([value]) => Some(new Rune(value))); - - const spacers = Tag.take( - Tag.SPACERS, - fields, - 1, - ([value]): Option => - u128.tryIntoU32(value).andThen((value) => (value <= MAX_SPACERS ? Some(value) : None)) - ); - - const symbol = Tag.take(Tag.SYMBOL, fields, 1, ([value]) => - u128.tryIntoU32(value).andThen((value) => { - try { - return Some(String.fromCodePoint(Number(value))); - } catch (e) { - return None; - } - }) - ); - - const termsResult = Flag.take(flags, Flag.TERMS); - const termsFlag = termsResult.set; - flags = termsResult.flags; - - const terms = termsFlag - ? (() => { - const amount = Tag.take(Tag.AMOUNT, fields, 1, ([value]) => Some(value)); - - const cap = Tag.take(Tag.CAP, fields, 1, ([value]) => Some(value)); - - const offset = [ - Tag.take(Tag.OFFSET_START, fields, 1, ([value]) => u128.tryIntoU64(value)), - Tag.take(Tag.OFFSET_END, fields, 1, ([value]) => u128.tryIntoU64(value)), - ] as const; - - const height = [ - Tag.take(Tag.HEIGHT_START, fields, 1, ([value]) => u128.tryIntoU64(value)), - Tag.take(Tag.HEIGHT_END, fields, 1, ([value]) => u128.tryIntoU64(value)), - ] as const; - - return Some({ amount, cap, offset, height }); - })() - : None; - - const premine = Tag.take(Tag.PREMINE, fields, 1, ([value]) => Some(value)); - - const turboResult = Flag.take(flags, Flag.TURBO); - const turbo = etchingResult.set; - flags = turboResult.flags; - - return Some(new Etching(divisibility, rune, spacers, symbol, terms, premine, turbo)); - })() - : None; - - const mint = Tag.take(Tag.MINT, fields, 2, ([block, tx]): Option => { - const optionBlockU64 = u128.tryIntoU64(block); - const optionTxU32 = u128.tryIntoU32(tx); - - if (optionBlockU64.isNone() || optionTxU32.isNone()) { - return None; - } - - return RuneId.new(optionBlockU64.unwrap(), optionTxU32.unwrap()); - }); - - const pointer = Tag.take( - Tag.POINTER, - fields, - 1, - ([value]): Option => - u128 - .tryIntoU32(value) - .andThen((value) => (value < transaction.vout.length ? Some(value) : None)) - ); - - if (etching.map((etching) => etching.supply.isNone()).unwrapOr(false)) { - flaws.push(Flaw.SUPPLY_OVERFLOW); - } - - if (flags !== 0n) { - flaws.push(Flaw.UNRECOGNIZED_FLAG); - } - - if ([...fields.keys()].find((tag) => tag % 2n === 0n) !== undefined) { - flaws.push(Flaw.UNRECOGNIZED_EVEN_TAG); - } - - if (flaws.length !== 0) { - return Some( - new Cenotaph( - flaws, - etching.andThen((etching) => etching.rune), - mint - ) - ); - } - - return Some(new Runestone(mint, pointer, edicts, etching)); - } - - static payload(transaction: RunestoneTx): Option { - // search transaction outputs for payload - for (const output of transaction.vout) { - const instructions = script.decompile(hexToBytes(output.scriptpubkey)); - if (instructions === null) { - throw new Error('unable to decompile'); - } - - // payload starts with OP_RETURN - let nextInstructionResult = instructions.next(); - if (nextInstructionResult.done || nextInstructionResult.value !== OP_RETURN) { - continue; - } - - // followed by the protocol identifier - nextInstructionResult = instructions.next(); - if ( - nextInstructionResult.done || - nextInstructionResult.value instanceof Uint8Array || - nextInstructionResult.value !== MAGIC_NUMBER - ) { - continue; - } - - // construct the payload by concatinating remaining data pushes - let payloads: Uint8Array[] = []; - - do { - nextInstructionResult = instructions.next(); - - if (nextInstructionResult.done) { - const decodedSuccessfully = nextInstructionResult.value; - if (!decodedSuccessfully) { - return Some(Flaw.INVALID_SCRIPT); - } - break; - } - - const instruction = nextInstructionResult.value; - if (instruction instanceof Uint8Array) { - payloads.push(instruction); - } else { - return Some(Flaw.OPCODE); - } - } while (true); - - return Some(concatUint8Arrays(payloads)); - } - - return None; - } - - static integers(payload: Uint8Array): Option { - const integers: u128[] = []; - - const seekArray = new SeekArray(payload); - while (!seekArray.isFinished()) { - const optionInt = u128.decodeVarInt(seekArray); - if (optionInt.isNone()) { - return None; - } - integers.push(optionInt.unwrap()); - } - - return Some(integers); - } -} diff --git a/frontend/src/app/shared/ord/rune/script.ts b/frontend/src/app/shared/ord/rune/script.ts deleted file mode 100644 index 67d579ab8..000000000 --- a/frontend/src/app/shared/ord/rune/script.ts +++ /dev/null @@ -1,237 +0,0 @@ -namespace pushdata { - /** - * Calculates the encoding length of a number used for push data in Bitcoin transactions. - * @param i The number to calculate the encoding length for. - * @returns The encoding length of the number. - */ - export function encodingLength(i: number): number { - return i < OPS.OP_PUSHDATA1 ? 1 : i <= 0xff ? 2 : i <= 0xffff ? 3 : 5; - } - - /** - * Decodes a byte array and returns information about the opcode, number, and size. - * @param array - The byte array to decode. - * @param offset - The offset within the array to start decoding. - * @returns An object containing the opcode, number, and size, or null if decoding fails. - */ - export function decode( - array: Uint8Array, - offset: number - ): { - opcode: number; - number: number; - size: number; - } | null { - const dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); - const opcode = dataView.getUint8(offset); - let num: number; - let size: number; - - // ~6 bit - if (opcode < OPS.OP_PUSHDATA1) { - num = opcode; - size = 1; - - // 8 bit - } else if (opcode === OPS.OP_PUSHDATA1) { - if (offset + 2 > array.length) return null; - num = dataView.getUint8(offset + 1); - size = 2; - - // 16 bit - } else if (opcode === OPS.OP_PUSHDATA2) { - if (offset + 3 > array.length) return null; - num = dataView.getUint16(offset + 1, true); // true for little-endian - size = 3; - - // 32 bit - } else { - if (offset + 5 > array.length) return null; - if (opcode !== OPS.OP_PUSHDATA4) throw new Error('Unexpected opcode'); - - num = dataView.getUint32(offset + 1, true); // true for little-endian - size = 5; - } - - return { - opcode, - number: num, - size, - }; - } -} - -const OPS = { - OP_FALSE: 0, - OP_0: 0, - OP_PUSHDATA1: 76, - OP_PUSHDATA2: 77, - OP_PUSHDATA4: 78, - OP_1NEGATE: 79, - OP_RESERVED: 80, - OP_TRUE: 81, - OP_1: 81, - OP_2: 82, - OP_3: 83, - OP_4: 84, - OP_5: 85, - OP_6: 86, - OP_7: 87, - OP_8: 88, - OP_9: 89, - OP_10: 90, - OP_11: 91, - OP_12: 92, - OP_13: 93, - OP_14: 94, - OP_15: 95, - OP_16: 96, - - OP_NOP: 97, - OP_VER: 98, - OP_IF: 99, - OP_NOTIF: 100, - OP_VERIF: 101, - OP_VERNOTIF: 102, - OP_ELSE: 103, - OP_ENDIF: 104, - OP_VERIFY: 105, - OP_RETURN: 106, - - OP_TOALTSTACK: 107, - OP_FROMALTSTACK: 108, - OP_2DROP: 109, - OP_2DUP: 110, - OP_3DUP: 111, - OP_2OVER: 112, - OP_2ROT: 113, - OP_2SWAP: 114, - OP_IFDUP: 115, - OP_DEPTH: 116, - OP_DROP: 117, - OP_DUP: 118, - OP_NIP: 119, - OP_OVER: 120, - OP_PICK: 121, - OP_ROLL: 122, - OP_ROT: 123, - OP_SWAP: 124, - OP_TUCK: 125, - - OP_CAT: 126, - OP_SUBSTR: 127, - OP_LEFT: 128, - OP_RIGHT: 129, - OP_SIZE: 130, - - OP_INVERT: 131, - OP_AND: 132, - OP_OR: 133, - OP_XOR: 134, - OP_EQUAL: 135, - OP_EQUALVERIFY: 136, - OP_RESERVED1: 137, - OP_RESERVED2: 138, - - OP_1ADD: 139, - OP_1SUB: 140, - OP_2MUL: 141, - OP_2DIV: 142, - OP_NEGATE: 143, - OP_ABS: 144, - OP_NOT: 145, - OP_0NOTEQUAL: 146, - OP_ADD: 147, - OP_SUB: 148, - OP_MUL: 149, - OP_DIV: 150, - OP_MOD: 151, - OP_LSHIFT: 152, - OP_RSHIFT: 153, - - OP_BOOLAND: 154, - OP_BOOLOR: 155, - OP_NUMEQUAL: 156, - OP_NUMEQUALVERIFY: 157, - OP_NUMNOTEQUAL: 158, - OP_LESSTHAN: 159, - OP_GREATERTHAN: 160, - OP_LESSTHANOREQUAL: 161, - OP_GREATERTHANOREQUAL: 162, - OP_MIN: 163, - OP_MAX: 164, - - OP_WITHIN: 165, - - OP_RIPEMD160: 166, - OP_SHA1: 167, - OP_SHA256: 168, - OP_HASH160: 169, - OP_HASH256: 170, - OP_CODESEPARATOR: 171, - OP_CHECKSIG: 172, - OP_CHECKSIGVERIFY: 173, - OP_CHECKMULTISIG: 174, - OP_CHECKMULTISIGVERIFY: 175, - - OP_NOP1: 176, - - OP_NOP2: 177, - OP_CHECKLOCKTIMEVERIFY: 177, - - OP_NOP3: 178, - OP_CHECKSEQUENCEVERIFY: 178, - - OP_NOP4: 179, - OP_NOP5: 180, - OP_NOP6: 181, - OP_NOP7: 182, - OP_NOP8: 183, - OP_NOP9: 184, - OP_NOP10: 185, - - OP_CHECKSIGADD: 186, - - OP_PUBKEYHASH: 253, - OP_PUBKEY: 254, - OP_INVALIDOPCODE: 255, -} as const; - -export const opcodes = OPS; - -export namespace script { - export type Instruction = number | Uint8Array; - - export function* decompile(array: Uint8Array): Generator { - let i = 0; - - while (i < array.length) { - const opcode = array[i]; - - // data chunk - if (opcode >= OPS.OP_0 && opcode <= OPS.OP_PUSHDATA4) { - const d = pushdata.decode(array, i); - - // did reading a pushDataInt fail? - if (d === null) return false; - i += d.size; - - // attempt to read too much data? - if (i + d.number > array.length) return false; - - const data = array.subarray(i, i + d.number); - i += d.number; - - yield data; - - // opcode - } else { - yield opcode; - - i += 1; - } - } - - return true; - } -} diff --git a/frontend/src/app/shared/ord/rune/seekarray.ts b/frontend/src/app/shared/ord/rune/seekarray.ts deleted file mode 100644 index 1f465cbd3..000000000 --- a/frontend/src/app/shared/ord/rune/seekarray.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * This class provides a way to read data sequentially from a Uint8Array with automatic cursor management. - * It utilizes DataView for handling multi-byte data types. - * - * This replaces the SeekBuffer from the original runestone-lib! - */ -export class SeekArray { - - public seekIndex: number = 0; - private dataView: DataView; - - /** - * Constructs a SeekArray instance. - * - * @param array - The Uint8Array from which data will be read. - */ - constructor(private array: Uint8Array) { - this.dataView = new DataView(array.buffer, array.byteOffset, array.byteLength); - } - - /** - * Reads an unsigned 8-bit integer from the current position and advances the seek index by 1 byte. - * - * @returns The read value or undefined if reading beyond the end of the array. - */ - readUInt8(): number | undefined { - if (this.isFinished()) { - return undefined; - } - const value = this.dataView.getUint8(this.seekIndex); - this.seekIndex += 1; - return value; - } - - /** - * Checks if the seek index has reached or surpassed the length of the underlying array. - * - * @returns true if there are no more bytes to read, false otherwise. - */ - isFinished(): boolean { - return this.seekIndex >= this.array.length; - } -} diff --git a/frontend/src/app/shared/ord/rune/spacedrune.ts b/frontend/src/app/shared/ord/rune/spacedrune.ts deleted file mode 100644 index b00b0da3a..000000000 --- a/frontend/src/app/shared/ord/rune/spacedrune.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Rune } from './rune'; - -export class SpacedRune { - constructor(readonly rune: Rune, readonly spacers: number) {} - - toString(): string { - const rune = this.rune.toString(); - let i = 0; - let result = ''; - for (const c of rune) { - result += c; - - if (i < rune.length - 1 && (this.spacers & (1 << i)) !== 0) { - result += '•'; - } - i++; - } - - return result; - } -} diff --git a/frontend/src/app/shared/ord/rune/tag.ts b/frontend/src/app/shared/ord/rune/tag.ts deleted file mode 100644 index 8e39925d4..000000000 --- a/frontend/src/app/shared/ord/rune/tag.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { None, Option, Some } from './monads'; -import { u128 } from './integer'; -import { FixedArray } from './utils'; - -export enum Tag { - BODY = 0, - FLAGS = 2, - RUNE = 4, - - PREMINE = 6, - CAP = 8, - AMOUNT = 10, - HEIGHT_START = 12, - HEIGHT_END = 14, - OFFSET_START = 16, - OFFSET_END = 18, - MINT = 20, - POINTER = 22, - CENOTAPH = 126, - - DIVISIBILITY = 1, - SPACERS = 3, - SYMBOL = 5, - NOP = 127, -} - -export namespace Tag { - export function take( - tag: Tag, - fields: Map, - n: N, - withFn: (values: FixedArray) => Option - ): Option { - const field = fields.get(u128(tag)); - if (field === undefined) { - return None; - } - - const values: u128[] = []; - for (const i of [...Array(n).keys()]) { - if (field[i] === undefined) { - return None; - } - values[i] = field[i]; - } - - const optionValue = withFn(values as FixedArray); - if (optionValue.isNone()) { - return None; - } - - field.splice(0, n); - - if (field.length === 0) { - fields.delete(u128(tag)); - } - - return Some(optionValue.unwrap()); - } -} diff --git a/frontend/src/app/shared/ord/rune/terms.ts b/frontend/src/app/shared/ord/rune/terms.ts deleted file mode 100644 index 464c166e0..000000000 --- a/frontend/src/app/shared/ord/rune/terms.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Option } from './monads'; -import { u128, u64 } from './integer'; - -export type Terms = { - amount: Option; - cap: Option; - height: readonly [Option, Option]; - offset: readonly [Option, Option]; -}; diff --git a/frontend/src/app/shared/ord/rune/utils.ts b/frontend/src/app/shared/ord/rune/utils.ts deleted file mode 100644 index a6fa8e0a1..000000000 --- a/frontend/src/app/shared/ord/rune/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -type GrowToSize = A['length'] extends N - ? A - : GrowToSize; - -export type FixedArray = GrowToSize; - From 65f080d5268b7365a0a0f428ddc340fca6a19d8d Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 11:24:17 +0900 Subject: [PATCH 77/94] FIx error handling logic in ord-data --- .../ord-data/ord-data.component.html | 104 ++++++++---------- .../components/ord-data/ord-data.component.ts | 2 - .../transactions-list.component.html | 4 +- 3 files changed, 50 insertions(+), 60 deletions(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html index 696e7ea17..97dbc0d9d 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.html +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -1,64 +1,56 @@ -@if (error) { -
- Error fetching data (code {{ error.status }}) -
-} @else { - @if (minted) { - - Mint - {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} - +@if (minted) { + + Mint + {{ minted >= 100000 ? (minted | amountShortener:undefined:undefined:true) : minted }} + + +} +@if (runestone?.etching?.supply) { + @if (runestone?.etching.premine > 0) { + + Premine + {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} + ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) + + } @else { + + Etching of + {{ runestone.etching.symbol }} + {{ runestone.etching.spacedName }} } - @if (runestone?.etching?.supply) { - @if (runestone?.etching.premine > 0) { - - Premine - {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} - {{ runestone.etching.symbol }} - {{ runestone.etching.spacedName }} - ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) - - } @else { - - Etching of - {{ runestone.etching.symbol }} - {{ runestone.etching.spacedName }} - - } - } - @if (transferredRunes?.length && type === 'vout') { -
- - Transfer - - -
- } +} +@if (transferredRunes?.length && type === 'vout') { +
+ + Transfer + + +
+} - @if (inscriptions?.length && type === 'vin') { -
-
- {{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }} - {{ contentType.value.totalSize | bytes:2:'B':undefined:true }} - - Source inscription - -
-
{{ contentType.value.json | json }}
-
{{ contentType.value.text }}
-
- } - - @if (!runestone && type === 'vout') { -
- } - - @if (!inscriptions?.length && type === 'vin') { +@if (inscriptions?.length && type === 'vin') { +
- Error decoding inscription data + {{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }} + {{ contentType.value.totalSize | bytes:2:'B':undefined:true }} + + Source inscription +
- } +
{{ contentType.value.json | json }}
+
{{ contentType.value.text }}
+
+} + +@if (!runestone && type === 'vout') { +
+} + +@if ((runestone && !minted && !runestone.etching?.supply && !transferredRunes?.length && type === 'vout') || (!inscriptions?.length && type === 'vin')) { + Error decoding data } diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index 233b8d243..ccc77bce6 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -1,5 +1,4 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { HttpErrorResponse } from '@angular/common/http'; import { Runestone, Etching } from '../../shared/ord/rune.utils'; export interface Inscription { @@ -20,7 +19,6 @@ export class OrdDataComponent implements OnChanges { @Input() inscriptions: Inscription[]; @Input() runestone: Runestone; @Input() runeInfo: { [id: string]: { etching: Etching; txid: string } }; - @Input() error: HttpErrorResponse; @Input() type: 'vin' | 'vout'; toNumber = (value: bigint): number => Number(value); diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 26187ecde..217eab7d7 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -103,7 +103,7 @@ }">
@@ -297,7 +297,7 @@ 'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address)) }"> From 15b3c88a1f56cdbd98a77b326ee2da4d5e9a4a7a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 8 Oct 2024 02:40:14 +0000 Subject: [PATCH 78/94] fix optional rune divisibility bug --- frontend/src/app/shared/ord/rune.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/ord/rune.utils.ts b/frontend/src/app/shared/ord/rune.utils.ts index a1f947b46..c36f3ef06 100644 --- a/frontend/src/app/shared/ord/rune.utils.ts +++ b/frontend/src/app/shared/ord/rune.utils.ts @@ -201,9 +201,9 @@ function messageToRunestone(message: Message): Runestone { if (flags & Flag.ETCHING) { const hasTerms = (flags & Flag.TERMS) > 0n; const isTurbo = (flags & Flag.TURBO) > 0n; - const name = parseRuneName(message.fields[Tag.Rune][0]); + const name = parseRuneName(message.fields[Tag.Rune]?.[0] ?? 0n); etching = { - divisibility: Number(message.fields[Tag.Divisibility][0]), + divisibility: Number(message.fields[Tag.Divisibility]?.[0] ?? 0n), premine: message.fields[Tag.Premine]?.[0], symbol: message.fields[Tag.Symbol]?.[0] ? String.fromCodePoint(Number(message.fields[Tag.Symbol][0])) : '¤', terms: hasTerms ? { From 040c067aac47d855284a90f9357d1f6b8d872cfd Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 8 Oct 2024 02:49:46 +0000 Subject: [PATCH 79/94] fix rune edict wrong id type bug --- frontend/src/app/shared/ord/rune.utils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/app/shared/ord/rune.utils.ts b/frontend/src/app/shared/ord/rune.utils.ts index c36f3ef06..c23a55264 100644 --- a/frontend/src/app/shared/ord/rune.utils.ts +++ b/frontend/src/app/shared/ord/rune.utils.ts @@ -154,10 +154,7 @@ function integersToMessage(integers: bigint[]): Message { const amount = integers.shift(); const output = integers.shift(); message.edicts.push({ - id: { - block: height, - index: txIndex, - }, + id: new RuneId(Number(height), Number(txIndex)), amount, output, }); From 177bbc83f3f73cd5dbcf98f8271c607e13250fab Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:38:12 +0900 Subject: [PATCH 80/94] Clean up etches fetching logic --- frontend/src/app/services/ord-api.service.ts | 63 ++++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts index da75a74af..6a38e5b17 100644 --- a/frontend/src/app/services/ord-api.service.ts +++ b/frontend/src/app/services/ord-api.service.ts @@ -19,15 +19,12 @@ export class OrdApiService { decodeRunestone$(tx: Transaction): Observable<{ runestone: Runestone, runeInfo: { [id: string]: { etching: Etching; txid: string; } } }> { const runestone = decipherRunestone(tx); const runeInfo: { [id: string]: { etching: Etching; txid: string; } } = {}; - const runesToFetch: Set = new Set(); if (runestone) { + const runesToFetch: Set = new Set(); + if (runestone.mint) { - if (runestone.mint.toString() === '1:0') { - runeInfo[runestone.mint.toString()] = { etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }; - } else { - runesToFetch.add(runestone.mint.toString()); - } + runesToFetch.add(runestone.mint.toString()); } if (runestone.edicts.length) { @@ -37,18 +34,15 @@ export class OrdApiService { } if (runesToFetch.size) { - const runeEtchingObservables = Array.from(runesToFetch).map(runeId => { - return this.getEtchingFromRuneId$(runeId).pipe( - tap(etching => { - if (etching) { - runeInfo[runeId] = etching; - } - }) - ); - }); + const runeEtchingObservables = Array.from(runesToFetch).map(runeId => this.getEtchingFromRuneId$(runeId)); return forkJoin(runeEtchingObservables).pipe( - map(() => { + map((etchings) => { + etchings.forEach((el) => { + if (el) { + runeInfo[el.runeId] = { etching: el.etching, txid: el.txid }; + } + }); return { runestone: runestone, runeInfo }; }) ); @@ -60,24 +54,27 @@ export class OrdApiService { } // Get etching from runeId by looking up the transaction that etched the rune - getEtchingFromRuneId$(runeId: string): Observable<{ etching: Etching; txid: string; }> { - const [blockNumber, txIndex] = runeId.split(':'); - - return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockNumber)).pipe( - switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), - switchMap(txId => this.electrsApiService.getTransaction$(txId)), - switchMap(tx => { - const runestone = decipherRunestone(tx); - if (runestone) { - const etching = runestone.etching; - if (etching) { - return of({ etching, txid: tx.txid }); + getEtchingFromRuneId$(runeId: string): Observable<{ runeId: string; etching: Etching; txid: string; }> { + if (runeId === '1:0') { + return of({ runeId, etching: UNCOMMON_GOODS, txid: '0000000000000000000000000000000000000000000000000000000000000000' }); + } else { + const [blockNumber, txIndex] = runeId.split(':'); + return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockNumber)).pipe( + switchMap(blockHash => this.electrsApiService.getBlockTxId$(blockHash, parseInt(txIndex))), + switchMap(txId => this.electrsApiService.getTransaction$(txId)), + switchMap(tx => { + const runestone = decipherRunestone(tx); + if (runestone) { + const etching = runestone.etching; + if (etching) { + return of({ runeId, etching, txid: tx.txid }); + } } - } - return of(null); - }), - catchError(() => of(null)) - ); + return of(null); + }), + catchError(() => of(null)) + ); + } } decodeInscriptions(witness: string): Inscription[] | null { From e440c3f235070e135713ada13b4299235cbf7f8a Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:40:25 +0900 Subject: [PATCH 81/94] Fix edicts displaying --- .../src/app/components/ord-data/ord-data.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index ccc77bce6..ccf8f6eae 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -34,10 +34,8 @@ export class OrdDataComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes.runestone && this.runestone) { - this.transferredRunes = Object.entries(this.runeInfo).map(([key, runeInfo]) => ({ key, ...runeInfo })); if (this.runestone.mint && this.runeInfo[this.runestone.mint.toString()]) { const mint = this.runestone.mint.toString(); - this.transferredRunes = this.transferredRunes.filter(rune => rune.key !== mint); const terms = this.runeInfo[mint].etching.terms; const amount = terms?.amount; const divisibility = this.runeInfo[mint].etching.divisibility; @@ -45,6 +43,12 @@ export class OrdDataComponent implements OnChanges { this.minted = this.getAmount(amount, divisibility); } } + + this.runestone.edicts.forEach(edict => { + if (this.runeInfo[edict.id.toString()]) { + this.transferredRunes.push({ key: edict.id.toString(), ...this.runeInfo[edict.id.toString()] }); + } + }); } if (changes.inscriptions && this.inscriptions) { From 0a614291760ca51d4664e5a8f2853385f3b26887 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:41:14 +0900 Subject: [PATCH 82/94] Increase inscription max height --- frontend/src/app/components/ord-data/ord-data.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.scss b/frontend/src/app/components/ord-data/ord-data.component.scss index 7cb2cdca6..b218359d9 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.scss +++ b/frontend/src/app/components/ord-data/ord-data.component.scss @@ -31,5 +31,5 @@ a.disabled { pre { margin-top: 5px; - max-height: 150px; + max-height: 200px; } \ No newline at end of file From 1ddb8a39c9aa9dab934e63d59f80093a95b8d696 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:50:56 +0900 Subject: [PATCH 83/94] Show text inscriptions up to 50kB --- frontend/src/app/components/ord-data/ord-data.component.ts | 3 ++- frontend/src/app/shared/ord/inscription.utils.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index ccf8f6eae..0e18750f1 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -3,6 +3,7 @@ import { Runestone, Etching } from '../../shared/ord/rune.utils'; export interface Inscription { body?: Uint8Array; + is_cropped?: boolean; body_length?: number; content_type?: Uint8Array; content_type_str?: string; @@ -68,7 +69,7 @@ export class OrdDataComponent implements OnChanges { } // Text / JSON data - if ((key.includes('text') || key.includes('json')) && inscription.body?.length && !this.inscriptionsData[key].text && !this.inscriptionsData[key].json) { + if ((key.includes('text') || key.includes('json')) && !inscription.is_cropped && !this.inscriptionsData[key].text && !this.inscriptionsData[key].json) { const decoder = new TextDecoder('utf-8'); const text = decoder.decode(inscription.body); try { diff --git a/frontend/src/app/shared/ord/inscription.utils.ts b/frontend/src/app/shared/ord/inscription.utils.ts index efa9e8fe8..e62f892d7 100644 --- a/frontend/src/app/shared/ord/inscription.utils.ts +++ b/frontend/src/app/shared/ord/inscription.utils.ts @@ -390,7 +390,8 @@ export function extractInscriptionData(raw: Uint8Array, pointer: number): Inscri return { content_type_str: contentType, - body: combinedData.slice(0, 150), // Limit body to 150 bytes for now + body: combinedData.slice(0, 50_000), // Limit body to 50 kB for now + is_cropped: combinedData.length > 50_000, body_length: combinedData.length, delegate_txid: getKnownFieldValue(fields, knownFields.delegate) ? bytesToHex(getKnownFieldValue(fields, knownFields.delegate).reverse()) : null }; From 57a05c80a25cd7d3510b169d7495652ecaaac495 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:53:18 +0900 Subject: [PATCH 84/94] Move inscription type to utils --- .../src/app/components/ord-data/ord-data.component.ts | 10 +--------- .../transactions-list/transactions-list.component.ts | 2 +- frontend/src/app/services/ord-api.service.ts | 2 +- frontend/src/app/shared/ord/inscription.utils.ts | 11 +++++++++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index 0e18750f1..40e189f7b 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -1,14 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Runestone, Etching } from '../../shared/ord/rune.utils'; - -export interface Inscription { - body?: Uint8Array; - is_cropped?: boolean; - body_length?: number; - content_type?: Uint8Array; - content_type_str?: string; - delegate_txid?: string; -} +import { Inscription } from '../../shared/ord/inscription.utils'; @Component({ selector: 'app-ord-data', diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index 706ee9684..7bb1604c6 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -12,7 +12,7 @@ import { ApiService } from '../../services/api.service'; import { PriceService } from '../../services/price.service'; import { StorageService } from '../../services/storage.service'; import { OrdApiService } from '../../services/ord-api.service'; -import { Inscription } from '../ord-data/ord-data.component'; +import { Inscription } from '../../shared/ord/inscription.utils'; import { Etching, Runestone } from '../../shared/ord/rune.utils'; @Component({ diff --git a/frontend/src/app/services/ord-api.service.ts b/frontend/src/app/services/ord-api.service.ts index 6a38e5b17..5fcd75298 100644 --- a/frontend/src/app/services/ord-api.service.ts +++ b/frontend/src/app/services/ord-api.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { catchError, forkJoin, map, Observable, of, switchMap, tap } from 'rxjs'; -import { Inscription } from '../components/ord-data/ord-data.component'; +import { Inscription } from '../shared/ord/inscription.utils'; import { Transaction } from '../interfaces/electrs.interface'; import { getNextInscriptionMark, hexToBytes, extractInscriptionData } from '../shared/ord/inscription.utils'; import { decipherRunestone, Runestone, Etching, UNCOMMON_GOODS } from '../shared/ord/rune.utils'; diff --git a/frontend/src/app/shared/ord/inscription.utils.ts b/frontend/src/app/shared/ord/inscription.utils.ts index e62f892d7..f4d92b206 100644 --- a/frontend/src/app/shared/ord/inscription.utils.ts +++ b/frontend/src/app/shared/ord/inscription.utils.ts @@ -1,8 +1,6 @@ // Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src // Utils functions to decode ord inscriptions -import { Inscription } from "../../components/ord-data/ord-data.component"; - export const OP_FALSE = 0x00; export const OP_IF = 0x63; export const OP_0 = 0x00; @@ -304,6 +302,15 @@ export function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array { ////////////////////////////// Inscription /////////////////////////// +export interface Inscription { + body?: Uint8Array; + is_cropped?: boolean; + body_length?: number; + content_type?: Uint8Array; + content_type_str?: string; + delegate_txid?: string; +} + /** * Extracts fields from the raw data until OP_0 is encountered. * From 3486c35f5e28db3d68060b7dd940a700b869a766 Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 12:59:36 +0900 Subject: [PATCH 85/94] 50kb -> 100kb --- frontend/src/app/shared/ord/inscription.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/ord/inscription.utils.ts b/frontend/src/app/shared/ord/inscription.utils.ts index f4d92b206..78095f22f 100644 --- a/frontend/src/app/shared/ord/inscription.utils.ts +++ b/frontend/src/app/shared/ord/inscription.utils.ts @@ -397,8 +397,8 @@ export function extractInscriptionData(raw: Uint8Array, pointer: number): Inscri return { content_type_str: contentType, - body: combinedData.slice(0, 50_000), // Limit body to 50 kB for now - is_cropped: combinedData.length > 50_000, + body: combinedData.slice(0, 100_000), // Limit body to 100 kB for now + is_cropped: combinedData.length > 100_000, body_length: combinedData.length, delegate_txid: getKnownFieldValue(fields, knownFields.delegate) ? bytesToHex(getKnownFieldValue(fields, knownFields.delegate).reverse()) : null }; From 1b2f1b38b45eae900b464e557a25d108dd03f80a Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 13:09:19 +0900 Subject: [PATCH 86/94] undefined -> unknown --- .../src/app/components/ord-data/ord-data.component.html | 6 +++++- frontend/src/app/components/ord-data/ord-data.component.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html index 97dbc0d9d..14f24d5f3 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.html +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -34,7 +34,11 @@ @if (inscriptions?.length && type === 'vin') {
- {{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }} + @if (contentType.key !== 'undefined') { + {{ contentType.value.count > 1 ? contentType.value.count + " " : "" }}{{ contentType.value?.tag || contentType.key }} + } @else { + Unknown + } {{ contentType.value.totalSize | bytes:2:'B':undefined:true }} Source inscription diff --git a/frontend/src/app/components/ord-data/ord-data.component.ts b/frontend/src/app/components/ord-data/ord-data.component.ts index 40e189f7b..6c6d2af20 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.ts +++ b/frontend/src/app/components/ord-data/ord-data.component.ts @@ -41,7 +41,7 @@ export class OrdDataComponent implements OnChanges { if (this.runeInfo[edict.id.toString()]) { this.transferredRunes.push({ key: edict.id.toString(), ...this.runeInfo[edict.id.toString()] }); } - }); + }); } if (changes.inscriptions && this.inscriptions) { From 933a204462b805e31bcec2a416010d51d493f734 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 8 Oct 2024 18:37:03 +0900 Subject: [PATCH 87/94] [accelerator] fee delta matches what the user accepted to pay in frontend --- .../accelerate-checkout.component.ts | 9 ++++++--- frontend/src/app/services/services-api.service.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts index 162594cd6..c6f442c84 100644 --- a/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts +++ b/frontend/src/app/components/accelerate-checkout/accelerate-checkout.component.ts @@ -525,7 +525,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { this.processing = false; @@ -624,7 +625,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, cardTag, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { this.processing = false; @@ -714,7 +716,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy { tokenResult.token, tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.referenceId, - this.accelerationUUID + this.accelerationUUID, + costUSD ).subscribe({ next: () => { this.processing = false; diff --git a/frontend/src/app/services/services-api.service.ts b/frontend/src/app/services/services-api.service.ts index 4d841521b..5a17026a9 100644 --- a/frontend/src/app/services/services-api.service.ts +++ b/frontend/src/app/services/services-api.service.ts @@ -135,16 +135,16 @@ export class ServicesApiServices { return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); } - accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } - accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } - accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { - return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); + accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) { + return this.httpClient.post(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD }); } getAccelerations$(): Observable { From 2a27ee0c7cbf3e0ac36f843593820c4a0e5b171c Mon Sep 17 00:00:00 2001 From: natsoni Date: Tue, 8 Oct 2024 19:20:08 +0900 Subject: [PATCH 88/94] Fix rune premine amount --- frontend/src/app/components/ord-data/ord-data.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/ord-data/ord-data.component.html b/frontend/src/app/components/ord-data/ord-data.component.html index 14f24d5f3..8e58e66a4 100644 --- a/frontend/src/app/components/ord-data/ord-data.component.html +++ b/frontend/src/app/components/ord-data/ord-data.component.html @@ -9,7 +9,7 @@ @if (runestone?.etching.premine > 0) { Premine - {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} + {{ getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) >= 100000 ? (getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) | amountShortener:undefined:undefined:true) : getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) }} {{ runestone.etching.symbol }} {{ runestone.etching.spacedName }} ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply) From 15b7e75b69d03eb1db058e3986c94e6c7c424b4a Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 9 Oct 2024 16:33:19 +0900 Subject: [PATCH 89/94] Fix wrong timespan in acc timeline --- .../acceleration-timeline/acceleration-timeline.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 16fd24c7f..93001a66b 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -43,12 +43,10 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { this.poolsData[pool.unique_id] = pool; } }); - - this.updateTimes(); - this.interval = window.setInterval(this.updateTimes.bind(this), 60000); } ngOnChanges(changes): void { + this.updateTimes(); // Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 // if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) { From c4004ba301078221fc3bdaa86c3a1a47d9be1e6d Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 9 Oct 2024 17:50:24 +0900 Subject: [PATCH 90/94] Clean up timeline code --- .../acceleration-timeline.component.html | 4 +--- .../acceleration-timeline.component.ts | 20 ------------------- 2 files changed, 1 insertion(+), 23 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 ba0d44884..ef3ace5ea 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.html @@ -9,7 +9,7 @@
@if (eta) { - ~ + ~ }
@@ -48,8 +48,6 @@
- } @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 93001a66b..14f533a05 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -14,16 +14,11 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { @Input() tx: Transaction; @Input() accelerationInfo: Acceleration; @Input() eta: ETA; - // A mined transaction has standard ETA and accelerated ETA undefined - // A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet) - @Input() standardETA: number; - @Input() acceleratedETA: number; acceleratedAt: number; now: number; accelerateRatio: number; useAbsoluteTime: boolean = false; - interval: number; firstSeenToAccelerated: number; acceleratedToMined: number; @@ -47,17 +42,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { ngOnChanges(changes): void { this.updateTimes(); - // Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 - - // if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) { - // if (changes?.eta?.currentValue) { - // if (changes?.acceleratedETA?.currentValue) { - // this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now)); - // } else if (changes?.standardETA?.currentValue) { - // this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now)); - // } - // } - // } } updateTimes(): void { @@ -66,10 +50,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { 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); - } onHover(event, status: string): void { if (status === 'seen') { From 22236bdabef549837cc749fa90fefb47c0c0d202 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 9 Oct 2024 18:17:17 +0900 Subject: [PATCH 91/94] Fix frontend build --- .../transaction/transaction.component.html | 2 +- .../transaction/transaction.component.ts | 16 ---------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index ec06dd5ad..abaeaeb06 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -169,7 +169,7 @@

Acceleration Timeline

- +
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 1306c432d..5fb9e5921 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -119,7 +119,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { txChanged$ = new BehaviorSubject(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself) isAccelerated$ = new BehaviorSubject(false); // refactor this to make isAccelerated an observable itself ETA$: Observable; - standardETA$: Observable; isCached: boolean = false; now = Date.now(); da$: Observable; @@ -883,21 +882,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.miningStats = stats; this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable }); - if (!this.tx.status?.confirmed) { - this.standardETA$ = combineLatest([ - this.stateService.mempoolBlocks$.pipe(startWith(null)), - this.stateService.difficultyAdjustment$.pipe(startWith(null)), - ]).pipe( - map(([mempoolBlocks, da]) => { - return this.etaService.calculateUnacceleratedETA( - this.tx, - mempoolBlocks, - da, - this.cpfpInfo, - ); - }) - ) - } } this.isAccelerated$.next(this.isAcceleration); } From 73e8ba3e47b8d0e38884b2c71d1f3400efbf6294 Mon Sep 17 00:00:00 2001 From: natsoni Date: Wed, 9 Oct 2024 20:52:23 +0900 Subject: [PATCH 92/94] Fix timestamps on acceleration timeline --- .../acceleration-timeline/acceleration-timeline.component.ts | 4 ++-- .../src/app/components/transaction/transaction.component.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 14f533a05..b0cf98d86 100644 --- a/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts +++ b/frontend/src/app/components/acceleration-timeline/acceleration-timeline.component.ts @@ -11,11 +11,11 @@ import { MiningService } from '../../services/mining.service'; }) export class AccelerationTimelineComponent implements OnInit, OnChanges { @Input() transactionTime: number; + @Input() acceleratedAt: number; @Input() tx: Transaction; @Input() accelerationInfo: Acceleration; @Input() eta: ETA; - acceleratedAt: number; now: number; accelerateRatio: number; useAbsoluteTime: boolean = false; @@ -31,7 +31,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges { ) {} ngOnInit(): void { - this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; + this.updateTimes(); this.miningService.getPools().subscribe(pools => { for (const pool of pools) { diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index abaeaeb06..056b27fc5 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -164,12 +164,12 @@
- +

Acceleration Timeline

- +
From cdb4580c6d2251c583678e4ce29fe91e11f2973f Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 10 Oct 2024 18:01:35 +0900 Subject: [PATCH 93/94] Set audit start heights on prod --- production/mempool-frontend-config.mainnet.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/production/mempool-frontend-config.mainnet.json b/production/mempool-frontend-config.mainnet.json index 84cde82cf..61a8c2c2a 100644 --- a/production/mempool-frontend-config.mainnet.json +++ b/production/mempool-frontend-config.mainnet.json @@ -10,6 +10,9 @@ "MEMPOOL_WEBSITE_URL": "https://mempool.space", "LIQUID_WEBSITE_URL": "https://liquid.network", "BISQ_WEBSITE_URL": "https://bisq.markets", + "MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911, + "TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829, + "SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609, "ITEMS_PER_PAGE": 25, "LIGHTNING": true, "ACCELERATOR": true, From 69a994afd59c3c7fc492d29932b40d678fdafc99 Mon Sep 17 00:00:00 2001 From: natsoni Date: Thu, 10 Oct 2024 20:53:19 +0900 Subject: [PATCH 94/94] Add license to inscriptions.utils.ts --- frontend/src/app/shared/ord/inscription.utils.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/app/shared/ord/inscription.utils.ts b/frontend/src/app/shared/ord/inscription.utils.ts index 78095f22f..08ecc316a 100644 --- a/frontend/src/app/shared/ord/inscription.utils.ts +++ b/frontend/src/app/shared/ord/inscription.utils.ts @@ -1,3 +1,19 @@ +/* +MIT License + +Copyright (c) 2024 HAUS HOPPE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +*/ + // Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src // Utils functions to decode ord inscriptions
- +
- +