From cea218b81a80a1cbc5ea3213cfae94b345d3fe9a Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 23 Jul 2023 11:05:49 +0900 Subject: [PATCH 01/30] Reset the supported browsers list --- frontend/.browserslistrc | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc index 80848532e..e6f1183e7 100644 --- a/frontend/.browserslistrc +++ b/frontend/.browserslistrc @@ -2,11 +2,15 @@ # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + # You can see what browsers were selected by your queries by running: # npx browserslist -> 0.5% -last 2 versions +last 2 Chrome versions +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions Firefox ESR -not dead -not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file From 442a4ff6e0bd724faa2a0c362008562f9ba367aa Mon Sep 17 00:00:00 2001 From: Felipe Knorr Kuhn Date: Sun, 23 Jul 2023 11:06:21 +0900 Subject: [PATCH 02/30] Fix tsconfig settigns for ES2022 --- frontend/tsconfig.base.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json index c3676addb..cd44cb6d9 100644 --- a/frontend/tsconfig.base.json +++ b/frontend/tsconfig.base.json @@ -7,15 +7,15 @@ "declaration": false, "downlevelIteration": true, "experimentalDecorators": true, - "module": "es2020", + "module": "ES2020", "moduleResolution": "node", "importHelpers": true, - "target": "es2020", + "target": "ES2022", "typeRoots": [ "node_modules/@types" ], "lib": [ - "es2018", + "ES2018", "dom", "dom.iterable" ] @@ -24,5 +24,6 @@ "fullTemplateTypeCheck": true, "strictInjectionParameters": true, "strictTemplates": true, + "useDefineForClassFields": false } } From 975ec772fa99d9a8d32d9a8fe73a708185a185c2 Mon Sep 17 00:00:00 2001 From: junderw Date: Sat, 22 Jul 2023 19:41:36 -0700 Subject: [PATCH 03/30] Use more reliable Github Action for Rust toolchain install. --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02131d800..6947a0f00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,9 +28,7 @@ jobs: registry-url: "https://registry.npmjs.org" - name: Install 1.70.x Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: 1.70 + uses: dtolnay/rust-toolchain@1.70 - name: Install if: ${{ matrix.flavor == 'dev'}} From 65dbafd2ec0b13873468671bda0bdc40f611efa1 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 22 Jul 2023 17:51:45 +0900 Subject: [PATCH 04/30] Support P2PK address types --- .../bitcoin/bitcoin-api-abstract-factory.ts | 2 + backend/src/api/bitcoin/bitcoin-api.ts | 8 +++ backend/src/api/bitcoin/bitcoin.routes.ts | 41 +++++++++++ backend/src/api/bitcoin/electrum-api.ts | 71 +++++++++++++++++++ .../src/api/bitcoin/esplora-api.interface.ts | 7 ++ backend/src/api/bitcoin/esplora-api.ts | 8 +++ frontend/src/app/bitcoin.utils.ts | 9 +++ .../address/address-preview.component.ts | 8 ++- .../components/address/address.component.scss | 1 + .../components/address/address.component.ts | 15 ++-- .../transactions-list.component.html | 15 +++- .../transactions-list.component.scss | 6 ++ .../src/app/interfaces/electrs.interface.ts | 16 +++++ .../src/app/services/electrs-api.service.ts | 33 ++++++++- 14 files changed, 227 insertions(+), 13 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 7b2802d1b..c233ed5d7 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -14,6 +14,8 @@ export interface AbstractBitcoinApi { $getAddress(address: string): Promise; $getAddressTransactions(address: string, lastSeenTxId: string): Promise; $getAddressPrefix(prefix: string): string[]; + $getScriptHash(scripthash: string): Promise; + $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index cbcb2c571..c045d8664 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -108,6 +108,14 @@ class BitcoinApi implements AbstractBitcoinApi { throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.'); } + $getScriptHash(scripthash: string): Promise { + throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.'); + } + + $getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise { + throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.'); + } + $getRawMempool(): Promise { return this.bitcoindClient.getRawMemPool(); } diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index babc0aa53..ffdb2e629 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -121,6 +121,8 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions) + .get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash', this.getScriptHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix) ; } @@ -567,6 +569,45 @@ class BitcoinRoutes { } } + 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.'); + return; + } + + try { + const addressData = await bitcoinApi.$getScriptHash(req.params.address); + 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); + } + res.status(500).send(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.'); + return; + } + + try { + let lastTxId: string = ''; + if (req.query.after_txid && typeof req.query.after_txid === 'string') { + lastTxId = req.query.after_txid; + } + const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.address, lastTxId); + 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); + return; + } + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getAddressPrefix(req: Request, res: Response) { try { const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); diff --git a/backend/src/api/bitcoin/electrum-api.ts b/backend/src/api/bitcoin/electrum-api.ts index 9d1ef46d3..07c58dbc9 100644 --- a/backend/src/api/bitcoin/electrum-api.ts +++ b/backend/src/api/bitcoin/electrum-api.ts @@ -126,6 +126,77 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi { } } + async $getScriptHash(scripthash: string): Promise { + try { + const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash); + let history = memoryCache.get('Scripthash_getHistory', scripthash); + if (!history) { + history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); + memoryCache.set('Scripthash_getHistory', scripthash, history, 2); + } + + const unconfirmed = history ? history.filter((h) => h.fee).length : 0; + + return { + 'scripthash': scripthash, + 'chain_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.confirmed ? balance.confirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0, + 'tx_count': (history?.length || 0) - unconfirmed, + }, + 'mempool_stats': { + 'funded_txo_count': 0, + 'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0, + 'spent_txo_count': 0, + 'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0, + 'tx_count': unconfirmed, + }, + 'electrum': true, + }; + } catch (e: any) { + throw new Error(typeof e === 'string' ? e : e && e.message || e); + } + } + + async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise { + try { + loadingIndicators.setProgress('address-' + scripthash, 0); + + const transactions: IEsploraApi.Transaction[] = []; + let history = memoryCache.get('Scripthash_getHistory', scripthash); + if (!history) { + history = await this.electrumClient.blockchainScripthash_getHistory(scripthash); + memoryCache.set('Scripthash_getHistory', scripthash, history, 2); + } + if (!history) { + throw new Error('failed to get scripthash history'); + } + history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999)); + + let startingIndex = 0; + if (lastSeenTxId) { + const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId); + if (pos) { + startingIndex = pos + 1; + } + } + const endIndex = Math.min(startingIndex + 10, history.length); + + for (let i = startingIndex; i < endIndex; i++) { + const tx = await this.$getRawTransaction(history[i].tx_hash, false, true); + transactions.push(tx); + loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100); + } + + return transactions; + } catch (e: any) { + loadingIndicators.setProgress('address-' + scripthash, 100); + throw new Error(typeof e === 'string' ? e : e && e.message || e); + } + } + private $getScriptHashBalance(scriptHash: string): Promise { return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash)); } diff --git a/backend/src/api/bitcoin/esplora-api.interface.ts b/backend/src/api/bitcoin/esplora-api.interface.ts index 5b86952b0..55abe1d34 100644 --- a/backend/src/api/bitcoin/esplora-api.interface.ts +++ b/backend/src/api/bitcoin/esplora-api.interface.ts @@ -99,6 +99,13 @@ export namespace IEsploraApi { electrum?: boolean; } + export interface ScriptHash { + scripthash: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; + electrum?: boolean; + } + export interface ChainStats { funded_txo_count: number; funded_txo_sum: number; diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index ee7fa4765..01294cc01 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -110,6 +110,14 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method getAddressTransactions not implemented.'); } + $getScriptHash(scripthash: string): Promise { + throw new Error('Method getAddress not implemented.'); + } + + $getScriptHashTransactions(scripthash: string, txId?: string): Promise { + throw new Error('Method getAddressTransactions not implemented.'); + } + $getAddressPrefix(prefix: string): string[] { throw new Error('Method not implemented.'); } diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 5419464a9..7ff0d9570 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -281,3 +281,12 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf' return false; } } + +export async function calcScriptHash$(script: string): Promise { + const buf = Uint8Array.from(script.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + const hashBuffer = await crypto.subtle.digest('SHA-256', buf); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray + .map((bytes) => bytes.toString(16).padStart(2, '0')) + .join(''); +} \ No newline at end of file diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts index 713f09f14..07ead8baa 100644 --- a/frontend/src/app/components/address/address-preview.component.ts +++ b/frontend/src/app/components/address/address-preview.component.ts @@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy { this.address = null; this.addressInfo = null; this.addressString = params.get('id') || ''; - if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) { this.addressString = this.addressString.toLowerCase(); } this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); - return this.electrsApiService.getAddress$(this.addressString) - .pipe( + return (this.addressString.match(/[a-f0-9]{130}/) + ? this.electrsApiService.getPubKeyAddress$(this.addressString) + : this.electrsApiService.getAddress$(this.addressString) + ).pipe( catchError((err) => { this.isLoadingAddress = false; this.error = err; diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index 37abcc49e..fe0729b94 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -81,6 +81,7 @@ h1 { top: 11px; } @media (min-width: 768px) { + max-width: calc(100% - 180px); top: 17px; } } diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 57439f983..ae1f6dbbe 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } 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, Transaction } from '../../interfaces/electrs.interface'; +import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; import { AudioService } from '../../services/audio.service'; @@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy { this.addressInfo = null; document.body.scrollTo(0, 0); this.addressString = params.get('id') || ''; - if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) { this.addressString = this.addressString.toLowerCase(); } this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); @@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy { .pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0)) ) .pipe( - switchMap(() => this.electrsApiService.getAddress$(this.addressString) - .pipe( + switchMap(() => ( + this.addressString.match(/[a-f0-9]{130}/) + ? this.electrsApiService.getPubKeyAddress$(this.addressString) + : this.electrsApiService.getAddress$(this.addressString) + ).pipe( catchError((err) => { this.isLoadingAddress = false; this.error = err; @@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy { this.updateChainStats(); this.isLoadingAddress = false; this.isLoadingTransactions = true; - return this.electrsApiService.getAddressTransactions$(address.address); + return address.is_pubkey + ? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac') + : this.electrsApiService.getAddressTransactions$(address.address); }), switchMap((transactions) => { this.tempTransactions = transactions; 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 b32afbfb3..3f88c61b0 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -56,7 +56,9 @@ Peg-in - P2PK + P2PK + + @@ -182,12 +184,19 @@ - + + + + P2PK + + + +
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 08d7d7486..7356bad0b 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.scss +++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss @@ -140,6 +140,12 @@ h2 { font-family: monospace; } +.p2pk-address { + display: inline-block; + margin-left: 1em; + max-width: 140px; +} + .grey-info-text { color:#6c757d; font-style: italic; diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index 2739d2b06..df19f7491 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -129,6 +129,22 @@ export interface Address { address: string; chain_stats: ChainStats; mempool_stats: MempoolStats; + is_pubkey?: boolean; +} + +export interface ScriptHash { + electrum?: boolean; + scripthash: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; +} + +export interface AddressOrScriptHash { + electrum?: boolean; + address?: string; + scripthash?: string; + chain_stats: ChainStats; + mempool_stats: MempoolStats; } export interface ChainStats { diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index c87018741..f866eb23d 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface'; +import { Observable, from, of, switchMap } from 'rxjs'; +import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface'; import { StateService } from './state.service'; import { BlockExtended } from '../interfaces/node-api.interface'; +import { calcScriptHash$ } from '../bitcoin.utils'; @Injectable({ providedIn: 'root' @@ -65,6 +66,24 @@ export class ElectrsApiService { return this.httpClient.get
(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); } + getPubKeyAddress$(pubkey: string): Observable
{ + return this.getScriptHash$('41' + pubkey + 'ac').pipe( + switchMap((scripthash: ScriptHash) => { + return of({ + ...scripthash, + address: pubkey, + is_pubkey: true, + }); + }) + ); + } + + getScriptHash$(script: string): Observable { + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash)) + ); + } + getAddressTransactions$(address: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -73,6 +92,16 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } + getScriptHashTransactions$(script: string, txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(calcScriptHash$(script)).pipe( + switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })), + ); + } + getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } From 0ce043cca9f63c51e3b9bf4685ce5fffc33f200d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 13:55:27 +0900 Subject: [PATCH 05/30] Fix esplora error messages --- backend/src/api/bitcoin/esplora-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 01294cc01..5bfff5730 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -111,11 +111,11 @@ class ElectrsApi implements AbstractBitcoinApi { } $getScriptHash(scripthash: string): Promise { - throw new Error('Method getAddress not implemented.'); + throw new Error('Method getScriptHash not implemented.'); } $getScriptHashTransactions(scripthash: string, txId?: string): Promise { - throw new Error('Method getAddressTransactions not implemented.'); + throw new Error('Method getScriptHashTransactions not implemented.'); } $getAddressPrefix(prefix: string): string[] { From 48b55eed468d9515e82518ebb31d5c41fc1080a4 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 13:55:52 +0900 Subject: [PATCH 06/30] improve script hex parsing validation --- frontend/src/app/bitcoin.utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index 7ff0d9570..c4af730f6 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -283,7 +283,10 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf' } export async function calcScriptHash$(script: string): Promise { - const buf = Uint8Array.from(script.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); + if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) { + throw new Error('script is not a valid hex string'); + } + const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16))); const hashBuffer = await crypto.subtle.digest('SHA-256', buf); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray From 0376467e6c9c211b7357cf61f0e1903cc631dbaa Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 14:00:39 +0900 Subject: [PATCH 07/30] highlight matching P2PK inputs --- .../transactions-list/transactions-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3f88c61b0..d1d0673fe 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -23,7 +23,7 @@ From 56127dce6a19039c126858953a28fcb2eb475ae3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 14:05:04 +0900 Subject: [PATCH 08/30] Add P2PK support to search bar --- .../src/app/components/search-form/search-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index ab42fe1f7..2361f8873 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit { } } - regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/; + regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/; regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; regexBlockheight = /^[0-9]{1,9}$/; From ae183210e036b0c9455d21abb5d922cd5e70f083 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 23 Jul 2023 14:43:43 +0900 Subject: [PATCH 09/30] Updating pubkey width on mobile and desktop --- .../transactions-list/transactions-list.component.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 7356bad0b..14559089a 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.scss +++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss @@ -143,7 +143,10 @@ h2 { .p2pk-address { display: inline-block; margin-left: 1em; - max-width: 140px; + max-width: 100px; + @media (min-width: 576px) { + max-width: 200px + } } .grey-info-text { From a1e05c0c37bdf848c51b502cd1286bc9117120fa Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 17:45:01 +0900 Subject: [PATCH 10/30] Lightning channel balance progress bars --- .../channel-close-box.component.html | 58 ++++++++---- .../channel-close-box.component.scss | 94 +++++++++++++++++++ .../channel-close-box.component.ts | 70 +++++++++++--- .../lightning/channel/channel.component.html | 4 +- 4 files changed, 195 insertions(+), 31 deletions(-) diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html index b5615324b..08a341de4 100644 --- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html +++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html @@ -1,19 +1,43 @@
- - - - - - - - - - - - - - - - -
Starting balance{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}?
Closing balance{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}?
+
+
Starting balance
+
+
{{ left.alias }}
+
{{ right.alias }}
+
+
+
+ {{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }} + {{ minStartingBalance | number : '1.0-0' }} +
+
+ {{ channel.capacity - maxStartingBalance | number : '1.0-0' }} - {{ channel.capacity - minStartingBalance | number : '1.0-0' }} + {{ channel.capacity - maxStartingBalance | number : '1.0-0' }} +
+
+
+
+
+
+
+
+
+
+
Closing balance
+
+
+ {{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }} + {{ minClosingBalance | number : '1.0-0' }} +
+
+ {{ channel.capacity - maxClosingBalance | number : '1.0-0' }} - {{ channel.capacity - minClosingBalance | number : '1.0-0' }} + {{ channel.capacity - maxClosingBalance | number : '1.0-0' }} +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss index a42871308..f55550eb3 100644 --- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss +++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss @@ -6,4 +6,98 @@ .box { margin-bottom: 20px; } +} + +.starting-balance, .closing-balance { + width: 100%; + + h5 { + text-align: center; + } +} + +.nodes { + display: none; + flex-direction: row; + align-items: baseline; + justify-content: space-between; + + @media (max-width: 768px) { + display: flex; + } +} + +.balances { + display: flex; + flex-direction: row; + align-items: baseline; + justify-content: space-between; + margin-bottom: 8px; + + .balance { + &.left { + text-align: start; + } + &.right { + text-align: end; + } + } +} + +.balance-bar { + width: 100%; + height: 2em; + position: relative; + + .bar { + position: absolute; + top: 0; + bottom: 0; + height: 100%; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &.left { + background: #105fb0; + } + &.center { + background: repeating-linear-gradient( + 60deg, + #105fb0 0, + #105fb0 12px, + #1a9436 12px, + #1a9436 24px + ); + } + &.right { + background: #1a9436; + } + + .value { + flex: 0; + white-space: nowrap; + } + + &.hide-value { + .value { + display: none; + } + } + } + + @media (max-width: 768px) { + height: 1em; + + .bar.center { + background: repeating-linear-gradient( + 60deg, + #105fb0 0, + #105fb0 8px, + #1a9436 8px, + #1a9436 16px + ) + } + } } \ No newline at end of file diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts index 05cc31434..ef42464eb 100644 --- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts +++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts @@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f }) export class ChannelCloseBoxComponent implements OnChanges { @Input() channel: any; - @Input() local: any; - @Input() remote: any; + @Input() left: any; + @Input() right: any; showStartingBalance: boolean = false; showClosingBalance: boolean = false; @@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges { minClosingBalance: number; maxClosingBalance: number; + startingBalanceStyle: { + left: string, + center: string, + right: string, + } = { + left: '', + center: '', + right: '', + }; + + closingBalanceStyle: { + left: string, + center: string, + right: string, + } = { + left: '', + center: '', + right: '', + }; + + hideStartingLeft: boolean = false; + hideStartingRight: boolean = false; + hideClosingLeft: boolean = false; + hideClosingRight: boolean = false; + constructor() { } ngOnChanges(changes: SimpleChanges): void { - if (this.channel && this.local && this.remote) { - this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio; - this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance; + let closingCapacity; + if (this.channel && this.left && this.right) { + this.showStartingBalance = (this.left.funding_balance || this.right.funding_balance) && this.channel.funding_ratio; + this.showClosingBalance = this.left.closing_balance || this.right.closing_balance; if (this.channel.single_funded) { - if (this.local.funding_balance) { + if (this.left.funding_balance) { this.minStartingBalance = this.channel.capacity; this.maxStartingBalance = this.channel.capacity; - } else if (this.remote.funding_balance) { + } else if (this.right.funding_balance) { this.minStartingBalance = 0; this.maxStartingBalance = 0; } } else { - this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio); - this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio)); + this.minStartingBalance = clampRound(0, this.channel.capacity, this.left.funding_balance * this.channel.funding_ratio); + this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.right.funding_balance * this.channel.funding_ratio)); } - const closingCapacity = this.channel.capacity - this.channel.closing_fee; - this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance); - this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance); + closingCapacity = this.channel.capacity - this.channel.closing_fee; + this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance); + this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance); // margin of error to account for 2 x 330 sat anchor outputs if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) { @@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges { this.showStartingBalance = false; this.showClosingBalance = false; } + + const startingMinPc = (this.minStartingBalance / this.channel.capacity) * 100; + const startingMaxPc = (this.maxStartingBalance / this.channel.capacity) * 100; + this.startingBalanceStyle = { + left: `left: 0%; right: ${100 - startingMinPc}%;`, + center: `left: ${startingMinPc}%; right: ${100 -startingMaxPc}%;`, + right: `left: ${startingMaxPc}%; right: 0%;`, + }; + this.hideStartingLeft = startingMinPc < 15; + this.hideStartingRight = startingMaxPc > 85; + + const closingMinPc = (this.minClosingBalance / closingCapacity) * 100; + const closingMaxPc = (this.maxClosingBalance / closingCapacity) * 100; + this.closingBalanceStyle = { + left: `left: 0%; right: ${100 - closingMinPc}%;`, + center: `left: ${closingMinPc}%; right: ${100 - closingMaxPc}%;`, + right: `left: ${closingMaxPc}%; right: 0%;`, + }; + this.hideClosingLeft = closingMinPc < 15; + this.hideClosingRight = closingMaxPc > 85; } } diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 2766f1d15..b9d9e09a4 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -75,14 +75,14 @@
-
-
+ +
From 02f361af7334ec8e0df6206c15d328fa7807f5cf Mon Sep 17 00:00:00 2001 From: wiz Date: Sun, 23 Jul 2023 22:21:53 +0900 Subject: [PATCH 11/30] Hotfix for CLN crash --- backend/src/api/lightning/clightning/clightning-convert.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 771dabcd7..02854a79b 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -257,8 +257,8 @@ async function buildIncompleteChannel(clChannel: any): Promise Date: Sun, 23 Jul 2023 22:35:32 +0900 Subject: [PATCH 12/30] Another hotfix for CLN crash --- backend/src/api/lightning/clightning/clightning-convert.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 02854a79b..55e4bd213 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -217,7 +217,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise Date: Mon, 24 Jul 2023 10:18:00 +0900 Subject: [PATCH 13/30] [search bar] auto focus only in dashboards --- .../mining-dashboard.component.ts | 10 +++++++-- .../search-form/search-form.component.html | 2 +- .../search-form/search-form.component.ts | 21 ++++++++++++++++--- .../src/app/dashboard/dashboard.component.ts | 10 ++++++--- frontend/src/app/services/state.service.ts | 2 ++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index df4713374..22d0e11fe 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,6 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '../../services/state.service'; @Component({ selector: 'app-mining-dashboard', @@ -8,10 +9,11 @@ import { WebsocketService } from '../../services/websocket.service'; styleUrls: ['./mining-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MiningDashboardComponent implements OnInit { +export class MiningDashboardComponent implements OnInit, AfterViewChecked { constructor( private seoService: SeoService, private websocketService: WebsocketService, + private stateService: StateService ) { this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`); } @@ -19,4 +21,8 @@ export class MiningDashboardComponent implements OnInit { ngOnInit(): void { this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); } + + ngAfterViewChecked(): void { + this.stateService.searchFocus$.next(true); + } } diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index cdfcfe015..3fc03c83a 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,7 +1,7 @@
- +
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 2361f8873..2fc25748e 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; +import { EventType, NavigationStart, Router } from '@angular/router'; import { AssetsService } from '../../services/assets.service'; import { StateService } from '../../services/state.service'; import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; @@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit { this.handleKeyDown($event); } + @ViewChild('searchInput') searchInput: ElementRef; + constructor( private formBuilder: UntypedFormBuilder, private router: Router, @@ -55,11 +57,24 @@ export class SearchFormComponent implements OnInit { private electrsApiService: ElectrsApiService, private apiService: ApiService, private relativeUrlPipe: RelativeUrlPipe, - private elementRef: ElementRef, - ) { } + private elementRef: ElementRef + ) { + } ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); + + this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page + if (e.type === EventType.NavigationStart) { + this.searchInput.nativeElement.blur(); + } + }); + + this.stateService.searchFocus$.subscribe(focus => { + if (this.searchInput && focus === true) { + this.searchInput.nativeElement.focus(); + } + }); this.searchForm = this.formBuilder.group({ searchText: ['', Validators.required], diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index aca3593d7..a9c0bb31c 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,7 +1,7 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { AfterViewChecked, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; -import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface'; +import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface'; import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; @@ -31,7 +31,7 @@ interface MempoolStatsData { styleUrls: ['./dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DashboardComponent implements OnInit, OnDestroy { +export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked { featuredAssets$: Observable; network$: Observable; mempoolBlocksData$: Observable; @@ -57,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy { private seoService: SeoService ) { } + ngAfterViewChecked(): void { + this.stateService.searchFocus$.next(true); + } + ngOnDestroy(): void { this.currencySubscription.unsubscribe(); this.websocketService.stopTrackRbfSummary(); diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 5ebca9ba1..2c4f06b49 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -138,6 +138,8 @@ export class StateService { fiatCurrency$: BehaviorSubject; rateUnits$: BehaviorSubject; + searchFocus$: Subject = new Subject(); + constructor( @Inject(PLATFORM_ID) private platformId: any, @Inject(LOCALE_ID) private locale: string, From 44f2217a6801d343988902212c5fd2a41274564b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 10:45:27 +0900 Subject: [PATCH 14/30] Fix typo which skips sigop calculation --- backend/src/api/mempool.ts | 2 +- backend/src/api/transaction-utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index bc3f33806..d988ea47a 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -86,7 +86,7 @@ class Mempool { this.mempoolCache = mempoolData; let count = 0; for (const txid of Object.keys(this.mempoolCache)) { - if (this.mempoolCache[txid].sigops == null || this.mempoolCache[txid].effectiveFeePerVsize == null) { + if (!this.mempoolCache[txid].sigops || this.mempoolCache[txid].effectiveFeePerVsize == null) { this.mempoolCache[txid] = transactionUtils.extendMempoolTransaction(this.mempoolCache[txid]); } if (this.mempoolCache[txid].order == null) { diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 849aff8f3..a48c9f259 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -74,7 +74,7 @@ class TransactionUtils { public extendMempoolTransaction(transaction: IEsploraApi.Transaction): MempoolTransactionExtended { const vsize = Math.ceil(transaction.weight / 4); const fractionalVsize = (transaction.weight / 4); - const sigops = Common.isLiquid() ? this.countSigops(transaction) : 0; + const sigops = !Common.isLiquid() ? this.countSigops(transaction) : 0; // https://github.com/bitcoin/bitcoin/blob/e9262ea32a6e1d364fb7974844fadc36f931f8c6/src/policy/policy.cpp#L295-L298 const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor const feePerVbytes = (transaction.fee || 0) / fractionalVsize; From da4a20cb8574916681602b0b995ca93fd2fe4b04 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 24 Jul 2023 11:35:46 +0900 Subject: [PATCH 15/30] [search bar] dont auto focus if touch screen --- .../mining-dashboard.component.ts | 2 +- .../src/app/dashboard/dashboard.component.ts | 2 +- frontend/src/app/services/state.service.ts | 7 +++++ .../src/app/shared/pipes/bytes-pipe/utils.ts | 26 +++++++++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index 22d0e11fe..c7670bc1e 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -23,6 +23,6 @@ export class MiningDashboardComponent implements OnInit, AfterViewChecked { } ngAfterViewChecked(): void { - this.stateService.searchFocus$.next(true); + this.stateService.focusSearchInputDesktop(); } } diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index a9c0bb31c..6d61953cf 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -58,7 +58,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked { ) { } ngAfterViewChecked(): void { - this.stateService.searchFocus$.next(true); + this.stateService.focusSearchInputDesktop(); } ngOnDestroy(): void { diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 2c4f06b49..bebe751d6 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -7,6 +7,7 @@ import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { filter, map, scan, shareReplay } from 'rxjs/operators'; import { StorageService } from './storage.service'; +import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils'; export interface MarkBlockState { blockHeight?: number; @@ -357,4 +358,10 @@ export class StateService { this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT); this.blocksSubject$.next(this.blocks); } + + focusSearchInputDesktop() { + if (!hasTouchScreen()) { + this.searchFocus$.next(true); + } + } } diff --git a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts index fc8c2b08f..86a1e1a1d 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts @@ -309,3 +309,29 @@ export function takeWhile(input: any[], predicate: CollectionPredicate) { return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) => !predicate(item, index, collection)); } + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent +export function hasTouchScreen(): boolean { + let hasTouchScreen = false; + if ('maxTouchPoints' in navigator) { + hasTouchScreen = navigator.maxTouchPoints > 0; + } else if ('msMaxTouchPoints' in navigator) { + // @ts-ignore + hasTouchScreen = navigator.msMaxTouchPoints > 0; + } else { + const mQ = matchMedia?.('(pointer:coarse)'); + if (mQ?.media === '(pointer:coarse)') { + hasTouchScreen = !!mQ.matches; + } else if ('orientation' in window) { + hasTouchScreen = true; // deprecated, but good fallback + } else { + // @ts-ignore - Only as a last resort, fall back to user agent sniffing + const UA = navigator.userAgent; + hasTouchScreen = + /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) || + /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); + } + } + console.log(hasTouchScreen); + return hasTouchScreen; +} \ No newline at end of file From 7db391d762514c3ed55fba67ba17f2b82729af7b Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Mon, 24 Jul 2023 11:51:15 +0900 Subject: [PATCH 16/30] [search bar] add missing autofocus on lightning dashboard --- .../lightning-dashboard/lightning-dashboard.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 6fa4b454c..adaa8d115 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; import { INodesRanking } from '../../interfaces/node-api.interface'; @@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service'; styleUrls: ['./lightning-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LightningDashboardComponent implements OnInit { +export class LightningDashboardComponent implements OnInit, AfterViewChecked { statistics$: Observable; nodesRanking$: Observable; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; @@ -30,4 +30,7 @@ export class LightningDashboardComponent implements OnInit { this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); } + ngAfterViewChecked(): void { + this.stateService.focusSearchInputDesktop(); + } } From ee2d8f8c5a4d42c2ef3700cc56b92caa3cc28637 Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 24 Jul 2023 13:21:06 +0900 Subject: [PATCH 17/30] Sanitize channel id search --- backend/src/api/explorer/channels.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index ab29ed2c2..0b1b914fd 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -80,7 +80,7 @@ class ChannelsApi { public async $searchChannelsById(search: string): Promise { try { - const searchStripped = search.replace('%', '') + '%'; + const searchStripped = search.replace(/[^0-9x]/g, '') + '%'; const query = `SELECT id, short_id, capacity, status FROM channels WHERE id LIKE ? OR short_id LIKE ? LIMIT 10`; const [rows]: any = await DB.query(query, [searchStripped, searchStripped]); return rows; From 202d4122b4d811dab03ab9eaf0b79c4df6b64be0 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 22 Jul 2023 14:09:11 +0900 Subject: [PATCH 18/30] load mempool txs in bulk from esplora --- .../bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 4 + backend/src/api/bitcoin/esplora-api.ts | 23 +++++ backend/src/api/mempool.ts | 93 ++++++++++++------- 4 files changed, 86 insertions(+), 35 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index c233ed5d7..d195b0eeb 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -3,6 +3,7 @@ import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { $getRawMempool(): Promise; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; + $getMempoolTransactions(expectedCount: number); $getTransactionHex(txId: string): Promise; $getBlockHeightTip(): Promise; $getBlockHashTip(): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index c045d8664..237c69834 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -59,6 +59,10 @@ class BitcoinApi implements AbstractBitcoinApi { }); } + $getMempoolTransactions(expectedCount: number): Promise { + return Promise.resolve([]); + } + $getTransactionHex(txId: string): Promise { return this.$getRawTransaction(txId, true) .then((tx) => tx.hex || ''); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 5bfff5730..34de4f94f 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -5,6 +5,8 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; +import JsonStream from 'JSONStream'; + const axiosConnection = axios.create({ httpAgent: new http.Agent({ keepAlive: true, }) }); @@ -69,6 +71,27 @@ class ElectrsApi implements AbstractBitcoinApi { return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId); } + async $getMempoolTransactions(expectedCount: number): Promise { + const transactions: IEsploraApi.Transaction[] = []; + let count = 0; + return new Promise((resolve, reject) => { + axiosConnection.get(config.ESPLORA.REST_API_URL + '/mempool/txs', { ...this.activeAxiosConfig, timeout: 60000, responseType: 'stream' }).then(response => { + response.data.pipe(JsonStream.parse('*')).on('data', transaction => { + count++; + if (count % 10000 === 0) { + logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); + } + transactions.push(transaction); + }).on('end', () => { + logger.info(`Fetched all ${count} of ${expectedCount} mempool transactions from esplora`); + resolve(transactions); + }).on('error', (err) => { + reject(err); + }); + }); + }); + } + $getTransactionHex(txId: string): Promise { return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex'); } diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index d988ea47a..d0e63ae78 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -1,5 +1,5 @@ import config from '../config'; -import bitcoinApi from './bitcoin/bitcoin-api-factory'; +import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import { MempoolTransactionExtended, TransactionExtended, VbytesPerSecond } from '../mempool.interfaces'; import logger from '../logger'; import { Common } from './common'; @@ -103,6 +103,16 @@ class Mempool { this.addToSpendMap(Object.values(this.mempoolCache)); } + public async $reloadMempool(expectedCount: number): Promise { + const rawTransactions = await bitcoinApi.$getMempoolTransactions(expectedCount); + logger.info(`Inserting loaded mempool transactions into local cache`); + for (const transaction of rawTransactions) { + const extendedTransaction = transactionUtils.extendMempoolTransaction(transaction); + this.mempoolCache[extendedTransaction.txid] = extendedTransaction; + } + logger.info(`Done inserting loaded mempool transactions into local cache`); + } + public async $updateMemPoolInfo() { this.mempoolInfo = await this.$getMempoolInfo(); } @@ -162,41 +172,54 @@ class Mempool { }; let intervalTimer = Date.now(); - for (const txid of transactions) { - if (!this.mempoolCache[txid]) { - try { - const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false); - this.updateTimerProgress(timer, 'fetched new transaction'); - this.mempoolCache[txid] = transaction; - if (this.inSync) { - this.txPerSecondArray.push(new Date().getTime()); - this.vBytesPerSecondArray.push({ - unixTime: new Date().getTime(), - vSize: transaction.vsize, - }); - } - hasChange = true; - newTransactions.push(transaction); - } catch (e: any) { - if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) { - this.missingTxCount++; - } - logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); - } - } - if (Date.now() - intervalTimer > 5_000) { - - if (this.inSync) { - // Break and restart mempool loop if we spend too much time processing - // new transactions that may lead to falling behind on block height - logger.debug('Breaking mempool loop because the 5s time limit exceeded.'); - break; - } else { - const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100; - logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`); - loadingIndicators.setProgress('mempool', progress); - intervalTimer = Date.now() + let loaded = false; + if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) { + logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`); + try { + await this.$reloadMempool(transactions.length); + loaded = true; + } catch (e) { + logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions'); + } + } + + if (!loaded) { + for (const txid of transactions) { + if (!this.mempoolCache[txid]) { + try { + const transaction = await transactionUtils.$getMempoolTransactionExtended(txid, false, false, false); + this.updateTimerProgress(timer, 'fetched new transaction'); + this.mempoolCache[txid] = transaction; + if (this.inSync) { + this.txPerSecondArray.push(new Date().getTime()); + this.vBytesPerSecondArray.push({ + unixTime: new Date().getTime(), + vSize: transaction.vsize, + }); + } + hasChange = true; + newTransactions.push(transaction); + } catch (e: any) { + if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) { + this.missingTxCount++; + } + logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e)); + } + } + + if (Date.now() - intervalTimer > 5_000) { + if (this.inSync) { + // Break and restart mempool loop if we spend too much time processing + // new transactions that may lead to falling behind on block height + logger.debug('Breaking mempool loop because the 5s time limit exceeded.'); + break; + } else { + const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100; + logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`); + loadingIndicators.setProgress('mempool', progress); + intervalTimer = Date.now() + } } } } From db715a1dbacd3498150d054e9a5426a328eddc89 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 23 Jul 2023 15:15:16 +0900 Subject: [PATCH 19/30] Switch to batch mempool/txs/:txid endpoint --- backend/src/api/bitcoin/esplora-api.ts | 40 +++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 34de4f94f..46b17a4d2 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -5,8 +5,6 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; -import JsonStream from 'JSONStream'; - const axiosConnection = axios.create({ httpAgent: new http.Agent({ keepAlive: true, }) }); @@ -74,22 +72,30 @@ class ElectrsApi implements AbstractBitcoinApi { async $getMempoolTransactions(expectedCount: number): Promise { const transactions: IEsploraApi.Transaction[] = []; let count = 0; - return new Promise((resolve, reject) => { - axiosConnection.get(config.ESPLORA.REST_API_URL + '/mempool/txs', { ...this.activeAxiosConfig, timeout: 60000, responseType: 'stream' }).then(response => { - response.data.pipe(JsonStream.parse('*')).on('data', transaction => { - count++; - if (count % 10000 === 0) { - logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); + let done = false; + let last_txid = ''; + while (!done) { + try { + const result = await this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txs' + (last_txid ? '/' + last_txid : '')); + if (result) { + for (const tx of result) { + transactions.push(tx); + count++; } - transactions.push(transaction); - }).on('end', () => { - logger.info(`Fetched all ${count} of ${expectedCount} mempool transactions from esplora`); - resolve(transactions); - }).on('error', (err) => { - reject(err); - }); - }); - }); + logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); + if (result.length > 0) { + last_txid = result[result.length - 1].txid; + } else { + done = true; + } + } else { + done = true; + } + } catch(err) { + logger.err('failed to fetch bulk mempool transactions from esplora'); + } + } + return transactions; } $getTransactionHex(txId: string): Promise { From e59c961f25317d4af2bf394a3116d0ddd16b6d17 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 14:59:51 +0900 Subject: [PATCH 20/30] Add electrs sync progress updates --- .../bitcoin/bitcoin-api-abstract-factory.ts | 2 +- backend/src/api/bitcoin/bitcoin-api.ts | 2 +- backend/src/api/bitcoin/esplora-api.ts | 29 ++-------------- backend/src/api/mempool.ts | 34 ++++++++++++++++--- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index d195b0eeb..f610ed883 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -3,7 +3,7 @@ import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { $getRawMempool(): Promise; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise; - $getMempoolTransactions(expectedCount: number); + $getMempoolTransactions(lastTxid: string); $getTransactionHex(txId: string): Promise; $getBlockHeightTip(): Promise; $getBlockHashTip(): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 237c69834..3ccea01ef 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -59,7 +59,7 @@ class BitcoinApi implements AbstractBitcoinApi { }); } - $getMempoolTransactions(expectedCount: number): Promise { + $getMempoolTransactions(lastTxid: string): Promise { return Promise.resolve([]); } diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 46b17a4d2..73a44a845 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -69,33 +69,8 @@ class ElectrsApi implements AbstractBitcoinApi { return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId); } - async $getMempoolTransactions(expectedCount: number): Promise { - const transactions: IEsploraApi.Transaction[] = []; - let count = 0; - let done = false; - let last_txid = ''; - while (!done) { - try { - const result = await this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txs' + (last_txid ? '/' + last_txid : '')); - if (result) { - for (const tx of result) { - transactions.push(tx); - count++; - } - logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); - if (result.length > 0) { - last_txid = result[result.length - 1].txid; - } else { - done = true; - } - } else { - done = true; - } - } catch(err) { - logger.err('failed to fetch bulk mempool transactions from esplora'); - } - } - return transactions; + async $getMempoolTransactions(lastSeenTxid?: string): Promise { + return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : '')); } $getTransactionHex(txId: string): Promise { diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index d0e63ae78..88533365e 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators'; import bitcoinClient from './bitcoin/bitcoin-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import rbfCache from './rbf-cache'; +import { IEsploraApi } from './bitcoin/esplora-api.interface'; class Mempool { private inSync: boolean = false; @@ -104,11 +105,34 @@ class Mempool { } public async $reloadMempool(expectedCount: number): Promise { - const rawTransactions = await bitcoinApi.$getMempoolTransactions(expectedCount); - logger.info(`Inserting loaded mempool transactions into local cache`); - for (const transaction of rawTransactions) { - const extendedTransaction = transactionUtils.extendMempoolTransaction(transaction); - this.mempoolCache[extendedTransaction.txid] = extendedTransaction; + let count = 0; + let done = false; + let last_txid; + loadingIndicators.setProgress('mempool', count / expectedCount * 100); + while (!done) { + try { + const result = await bitcoinApi.$getMempoolTransactions(last_txid); + if (result) { + for (const tx of result) { + const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); + this.mempoolCache[extendedTransaction.txid] = extendedTransaction; + count++; + } + logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); + if (result.length > 0) { + last_txid = result[result.length - 1].txid; + } else { + done = true; + } + if (count < expectedCount) { + loadingIndicators.setProgress('mempool', count / expectedCount * 100); + } + } else { + done = true; + } + } catch(err) { + logger.err('failed to fetch bulk mempool transactions from esplora'); + } } logger.info(`Done inserting loaded mempool transactions into local cache`); } From de4265a6d1b616555d271540ff899fb4db9919e6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 16:19:19 +0900 Subject: [PATCH 21/30] More conservative mempool inSync status --- backend/src/api/mempool.ts | 15 ++++++++------- backend/src/api/websocket-handler.ts | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 88533365e..9239d6b3b 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -124,7 +124,7 @@ class Mempool { } else { done = true; } - if (count < expectedCount) { + if (Math.floor(count / expectedCount) < 1) { loadingIndicators.setProgress('mempool', count / expectedCount * 100); } } else { @@ -199,6 +199,7 @@ class Mempool { let loaded = false; if (config.MEMPOOL.BACKEND === 'esplora' && currentMempoolSize < transactions.length * 0.5 && transactions.length > 20_000) { + this.inSync = false; logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`); try { await this.$reloadMempool(transactions.length); @@ -293,12 +294,6 @@ class Mempool { const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); - if (!this.inSync && transactions.length === newMempoolSize) { - this.inSync = true; - logger.notice('The mempool is now in sync!'); - loadingIndicators.setProgress('mempool', 100); - } - this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize); if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) { @@ -310,6 +305,12 @@ class Mempool { this.updateTimerProgress(timer, 'completed async mempool callback'); } + if (!this.inSync && transactions.length === newMempoolSize) { + this.inSync = true; + logger.notice('The mempool is now in sync!'); + loadingIndicators.setProgress('mempool', 100); + } + const end = new Date().getTime(); const time = end - start; logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index a0c031175..ccaeb4a8b 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -644,7 +644,7 @@ class WebsocketHandler { memPool.handleMinedRbfTransactions(rbfTransactions); memPool.removeFromSpendMap(transactions); - if (config.MEMPOOL.AUDIT) { + if (config.MEMPOOL.AUDIT && memPool.isInSync()) { let projectedBlocks; let auditMempool = _memPool; // template calculation functions have mempool side effects, so calculate audits using @@ -665,7 +665,7 @@ class WebsocketHandler { projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions(); } - if (Common.indexingEnabled() && memPool.isInSync()) { + if (Common.indexingEnabled()) { const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool); const matchRate = Math.round(score * 100 * 100) / 100; From a6edfcc2723b40172d4006123e3170b1feb19d30 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 16:22:35 +0900 Subject: [PATCH 22/30] show mempool skeleton while not inSync --- .../mempool-blocks/mempool-blocks.component.ts | 9 ++++++++- frontend/src/app/services/state.service.ts | 1 + frontend/src/app/services/websocket.service.ts | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index e6d5a4bf6..71075b261 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -117,7 +117,14 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { }); this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks); this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); - this.loadingBlocks$ = this.stateService.isLoadingWebSocket$; + this.loadingBlocks$ = combineLatest([ + this.stateService.isLoadingWebSocket$, + this.stateService.isLoadingMempool$ + ]).pipe( + switchMap(([loadingBlocks, loadingMempool]) => { + return of(loadingBlocks || loadingMempool); + }) + ); this.mempoolBlocks$ = merge( of(true), diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 5ebca9ba1..ea00f12ab 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -113,6 +113,7 @@ export class StateService { mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>(); blockTransactions$ = new Subject(); isLoadingWebSocket$ = new ReplaySubject(1); + isLoadingMempool$ = new BehaviorSubject(true); vbytesPerSecond$ = new ReplaySubject(1); previousRetarget$ = new ReplaySubject(1); backendInfo$ = new ReplaySubject(1); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index f32f772ac..e70424cdc 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -368,6 +368,11 @@ export class WebsocketService { if (response.loadingIndicators) { this.stateService.loadingIndicators$.next(response.loadingIndicators); + if (response.loadingIndicators.mempool != null && response.loadingIndicators.mempool < 100) { + this.stateService.isLoadingMempool$.next(true); + } else { + this.stateService.isLoadingMempool$.next(false); + } } if (response.mempoolInfo) { From 2d463326e05d28db251dedacb3418605fe27c5d3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 17:22:38 +0900 Subject: [PATCH 23/30] fix gbt mempool size mismatch bug --- backend/src/api/mempool.ts | 13 +++++++++---- backend/src/api/websocket-handler.ts | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 9239d6b3b..b6ac9e074 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -104,10 +104,11 @@ class Mempool { this.addToSpendMap(Object.values(this.mempoolCache)); } - public async $reloadMempool(expectedCount: number): Promise { + public async $reloadMempool(expectedCount: number): Promise { let count = 0; let done = false; let last_txid; + const newTransactions: MempoolTransactionExtended[] = []; loadingIndicators.setProgress('mempool', count / expectedCount * 100); while (!done) { try { @@ -115,7 +116,10 @@ class Mempool { if (result) { for (const tx of result) { const extendedTransaction = transactionUtils.extendMempoolTransaction(tx); - this.mempoolCache[extendedTransaction.txid] = extendedTransaction; + if (!this.mempoolCache[extendedTransaction.txid]) { + newTransactions.push(extendedTransaction); + this.mempoolCache[extendedTransaction.txid] = extendedTransaction; + } count++; } logger.info(`Fetched ${count} of ${expectedCount} mempool transactions from esplora`); @@ -134,6 +138,7 @@ class Mempool { logger.err('failed to fetch bulk mempool transactions from esplora'); } } + return newTransactions; logger.info(`Done inserting loaded mempool transactions into local cache`); } @@ -177,7 +182,7 @@ class Mempool { const currentMempoolSize = Object.keys(this.mempoolCache).length; this.updateTimerProgress(timer, 'got raw mempool'); const diff = transactions.length - currentMempoolSize; - const newTransactions: MempoolTransactionExtended[] = []; + let newTransactions: MempoolTransactionExtended[] = []; this.mempoolCacheDelta = Math.abs(diff); @@ -202,7 +207,7 @@ class Mempool { this.inSync = false; logger.info(`Missing ${transactions.length - currentMempoolSize} mempool transactions, attempting to reload in bulk from esplora`); try { - await this.$reloadMempool(transactions.length); + newTransactions = await this.$reloadMempool(transactions.length); loaded = true; } catch (e) { logger.err('failed to load mempool in bulk from esplora, falling back to fetching individual transactions'); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index ccaeb4a8b..56c8513cd 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -604,7 +604,7 @@ class WebsocketHandler { } } - if (client['track-mempool-block'] >= 0) { + if (client['track-mempool-block'] >= 0 && memPool.isInSync()) { const index = client['track-mempool-block']; if (mBlockDeltas[index]) { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, { @@ -858,7 +858,7 @@ class WebsocketHandler { } } - if (client['track-mempool-block'] >= 0) { + if (client['track-mempool-block'] >= 0 && memPool.isInSync()) { const index = client['track-mempool-block']; if (mBlockDeltas && mBlockDeltas[index]) { response['projected-block-transactions'] = getCachedResponse(`projected-block-transactions-${index}`, { From 36fe5627c70a8ba9b350244f49c5228a000ac62c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 17:49:34 +0900 Subject: [PATCH 24/30] fix mempool sync skeleton loaders on Core backend --- backend/src/api/mempool.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index b6ac9e074..945b78738 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -128,7 +128,7 @@ class Mempool { } else { done = true; } - if (Math.floor(count / expectedCount) < 1) { + if (Math.floor((count / expectedCount) * 100) < 100) { loadingIndicators.setProgress('mempool', count / expectedCount * 100); } } else { @@ -247,7 +247,9 @@ class Mempool { } else { const progress = (currentMempoolSize + newTransactions.length) / transactions.length * 100; logger.debug(`Mempool is synchronizing. Processed ${newTransactions.length}/${diff} txs (${Math.round(progress)}%)`); - loadingIndicators.setProgress('mempool', progress); + if (Math.floor(progress) < 100) { + loadingIndicators.setProgress('mempool', progress); + } intervalTimer = Date.now() } } From 0ebfd6f0170279794e6016b68e57b255423dfa64 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 24 Jul 2023 16:58:30 +0900 Subject: [PATCH 25/30] Fetch block txs from mempool/electrs in bulk --- .../bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 4 ++ backend/src/api/bitcoin/esplora-api.ts | 4 ++ backend/src/api/blocks.ts | 66 +++++++++---------- backend/src/api/transaction-utils.ts | 2 +- 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index f610ed883..7f4a5e53a 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -8,6 +8,7 @@ export interface AbstractBitcoinApi { $getBlockHeightTip(): Promise; $getBlockHashTip(): Promise; $getTxIdsForBlock(hash: string): Promise; + $getTxsForBlock(hash: string): Promise; $getBlockHash(height: number): Promise; $getBlockHeader(hash: string): Promise; $getBlock(hash: string): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 3ccea01ef..a1cf767d9 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -81,6 +81,10 @@ class BitcoinApi implements AbstractBitcoinApi { .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); } + $getTxsForBlock(hash: string): Promise { + throw new Error('Method getTxsForBlock not supported by the Bitcoin RPC API.'); + } + $getRawBlock(hash: string): Promise { return this.bitcoindClient.getBlock(hash, 0) .then((raw: string) => Buffer.from(raw, "hex")); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index 73a44a845..ff10751e0 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -89,6 +89,10 @@ class ElectrsApi implements AbstractBitcoinApi { return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); } + $getTxsForBlock(hash: string): Promise { + return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txs'); + } + $getBlockHash(height: number): Promise { return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block-height/' + height); } diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 7cd37f637..1f1c4ebca 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -80,40 +80,38 @@ class Blocks { quiet: boolean = false, addMempoolData: boolean = false, ): Promise { - const transactions: TransactionExtended[] = []; - if (!txIds) { - txIds = await bitcoinApi.$getTxIdsForBlock(blockHash); - } + let transactions: TransactionExtended[] = []; const mempool = memPool.getMempool(); let transactionsFound = 0; let transactionsFetched = 0; - for (let i = 0; i < txIds.length; i++) { - if (mempool[txIds[i]]) { - // We update blocks before the mempool (index.ts), therefore we can - // optimize here by directly fetching txs in the "outdated" mempool - transactions.push(mempool[txIds[i]]); - transactionsFound++; - } else if (config.MEMPOOL.BACKEND === 'esplora' || !memPool.hasPriority() || i === 0) { - // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...) - if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam - logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); - } - try { - const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData); - transactions.push(tx); - transactionsFetched++; - } catch (e) { + if (config.MEMPOOL.BACKEND === 'esplora') { + const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash); + transactions = rawTransactions.map(tx => transactionUtils.extendTransaction(tx)); + + if (!quiet) { + logger.debug(`${transactions.length} fetched through backend service.`); + } + } else { + if (!txIds) { + txIds = await bitcoinApi.$getTxIdsForBlock(blockHash); + } + for (let i = 0; i < txIds.length; i++) { + if (mempool[txIds[i]]) { + // We update blocks before the mempool (index.ts), therefore we can + // optimize here by directly fetching txs in the "outdated" mempool + transactions.push(mempool[txIds[i]]); + transactionsFound++; + } else if (!memPool.hasPriority() || i === 0) { + // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...) + if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam + logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); + } try { - if (config.MEMPOOL.BACKEND === 'esplora') { - // Try again with core - const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true, addMempoolData); - transactions.push(tx); - transactionsFetched++; - } else { - throw e; - } + const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData); + transactions.push(tx); + transactionsFetched++; } catch (e) { if (i === 0) { const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e); @@ -124,17 +122,17 @@ class Blocks { } } } + + if (onlyCoinbase === true) { + break; // Fetch the first transaction and exit + } } - if (onlyCoinbase === true) { - break; // Fetch the first transaction and exit + if (!quiet) { + logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); } } - if (!quiet) { - logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); - } - return transactions; } diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index a48c9f259..0b10afdfb 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -53,7 +53,7 @@ class TransactionUtils { return (await this.$getTransactionExtended(txId, addPrevouts, lazyPrevouts, forceCore, true)) as MempoolTransactionExtended; } - private extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { + public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended { // @ts-ignore if (transaction.vsize) { // @ts-ignore From 25925751eb0f599b5df5fc865a56f27f189b9e3c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 25 Jul 2023 11:33:13 +0900 Subject: [PATCH 26/30] refactor $getTransactionsExtended to optimise API requests --- backend/src/api/blocks.ts | 114 ++++++++++++++++++++++---------------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 1f1c4ebca..4dbf4305e 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -70,6 +70,9 @@ class Blocks { * @param blockHash * @param blockHeight * @param onlyCoinbase - Set to true if you only need the coinbase transaction + * @param txIds - optional ordered list of transaction ids if already known + * @param quiet - don't print non-essential logs + * @param addMempoolData - calculate sigops etc * @returns Promise */ private async $getTransactionsExtended( @@ -80,60 +83,77 @@ class Blocks { quiet: boolean = false, addMempoolData: boolean = false, ): Promise { - let transactions: TransactionExtended[] = []; + const isEsplora = config.MEMPOOL.BACKEND === 'esplora'; + const transactionMap: { [txid: string]: TransactionExtended } = {}; + + if (!txIds) { + txIds = await bitcoinApi.$getTxIdsForBlock(blockHash); + } const mempool = memPool.getMempool(); - let transactionsFound = 0; - let transactionsFetched = 0; + let foundInMempool = 0; + let totalFound = 0; - if (config.MEMPOOL.BACKEND === 'esplora') { - const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash); - transactions = rawTransactions.map(tx => transactionUtils.extendTransaction(tx)); - - if (!quiet) { - logger.debug(`${transactions.length} fetched through backend service.`); - } - } else { - if (!txIds) { - txIds = await bitcoinApi.$getTxIdsForBlock(blockHash); - } - for (let i = 0; i < txIds.length; i++) { - if (mempool[txIds[i]]) { - // We update blocks before the mempool (index.ts), therefore we can - // optimize here by directly fetching txs in the "outdated" mempool - transactions.push(mempool[txIds[i]]); - transactionsFound++; - } else if (!memPool.hasPriority() || i === 0) { - // Otherwise we fetch the tx data through backend services (esplora, electrum, core rpc...) - if (!quiet && (i % (Math.round((txIds.length) / 10)) === 0 || i + 1 === txIds.length)) { // Avoid log spam - logger.debug(`Indexing tx ${i + 1} of ${txIds.length} in block #${blockHeight}`); - } - try { - const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, false, addMempoolData); - transactions.push(tx); - transactionsFetched++; - } catch (e) { - if (i === 0) { - const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e); - logger.err(msg); - throw new Error(msg); - } else { - logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e)); - } - } + // Copy existing transactions from the mempool + if (!onlyCoinbase) { + for (const txid of txIds) { + if (mempool[txid]) { + transactionMap[txid] = mempool[txid]; + foundInMempool++; + totalFound++; } - - if (onlyCoinbase === true) { - break; // Fetch the first transaction and exit - } - } - - if (!quiet) { - logger.debug(`${transactionsFound} of ${txIds.length} found in mempool. ${transactionsFetched} fetched through backend service.`); } } - return transactions; + // Skip expensive lookups while mempool has priority + if (onlyCoinbase) { + try { + const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData); + return [coinbase]; + } catch (e) { + const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e); + logger.err(msg); + throw new Error(msg); + } + } + + // Fetch remaining txs in bulk + if (isEsplora && (txIds.length - totalFound > 500)) { + try { + const rawTransactions = await bitcoinApi.$getTxsForBlock(blockHash); + for (const tx of rawTransactions) { + if (!transactionMap[tx.txid]) { + transactionMap[tx.txid] = addMempoolData ? transactionUtils.extendMempoolTransaction(tx) : transactionUtils.extendTransaction(tx); + totalFound++; + } + } + } catch (e) { + logger.err(`Cannot fetch bulk txs for block ${blockHash}. Reason: ` + (e instanceof Error ? e.message : e)); + } + } + + // Fetch remaining txs individually + for (const txid of txIds.filter(txid => !transactionMap[txid])) { + if (!transactionMap[txid]) { + if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam + logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`); + } + try { + const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData); + transactionMap[txid] = tx; + totalFound++; + } catch (e) { + logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e)); + } + } + } + + if (!quiet) { + logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`); + } + + // Return list of transactions, preserving block order + return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null); } /** From d7b874ac49a1416a71b23cb547a0d3153215a84f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 25 Jul 2023 14:00:17 +0900 Subject: [PATCH 27/30] Exclude all conflicting transactions from audit score --- backend/src/api/audit.ts | 13 +++++++------ backend/src/api/rbf-cache.ts | 18 ++++++++++++++++++ .../components/block-overview-graph/tx-view.ts | 4 ++-- .../block-overview-tooltip.component.html | 2 +- .../app/components/block/block.component.ts | 12 ++++++------ .../src/app/interfaces/node-api.interface.ts | 2 +- .../src/app/interfaces/websocket.interface.ts | 2 +- 7 files changed, 36 insertions(+), 17 deletions(-) diff --git a/backend/src/api/audit.ts b/backend/src/api/audit.ts index f7aecfca8..a909fc2b6 100644 --- a/backend/src/api/audit.ts +++ b/backend/src/api/audit.ts @@ -15,7 +15,7 @@ class Audit { const matches: string[] = []; // present in both mined block and template const added: string[] = []; // present in mined block, not in template const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN - const fullrbf: string[] = []; // either missing or present, and part of a fullrbf replacement + const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block const isCensored = {}; // missing, without excuse const isDisplaced = {}; let displacedWeight = 0; @@ -36,8 +36,9 @@ class Audit { // look for transactions that were expected in the template, but missing from the mined block for (const txid of projectedBlocks[0].transactionIds) { if (!inBlock[txid]) { - if (rbfCache.isFullRbf(txid)) { - fullrbf.push(txid); + // allow missing transactions which either belong to a full rbf tree, or conflict with any transaction in the mined block + if (rbfCache.has(txid) && (rbfCache.isFullRbf(txid) || rbfCache.anyInSameTree(txid, (tx) => inBlock[tx.txid]))) { + rbf.push(txid); } else if (mempool[txid]?.firstSeen != null && (now - (mempool[txid]?.firstSeen || 0)) <= PROPAGATION_MARGIN) { // tx is recent, may have reached the miner too late for inclusion fresh.push(txid); @@ -98,8 +99,8 @@ class Audit { if (inTemplate[tx.txid]) { matches.push(tx.txid); } else { - if (rbfCache.isFullRbf(tx.txid)) { - fullrbf.push(tx.txid); + if (rbfCache.has(tx.txid)) { + rbf.push(tx.txid); } else if (!isDisplaced[tx.txid]) { added.push(tx.txid); } @@ -147,7 +148,7 @@ class Audit { added, fresh, sigop: [], - fullrbf, + fullrbf: rbf, score, similarity, }; diff --git a/backend/src/api/rbf-cache.ts b/backend/src/api/rbf-cache.ts index 367ba1c0e..f28dd0de3 100644 --- a/backend/src/api/rbf-cache.ts +++ b/backend/src/api/rbf-cache.ts @@ -100,6 +100,24 @@ class RbfCache { this.dirtyTrees.add(treeId); } + public has(txId: string): boolean { + return this.txs.has(txId); + } + + public anyInSameTree(txId: string, predicate: (tx: RbfTransaction) => boolean): boolean { + const tree = this.getRbfTree(txId); + if (!tree) { + return false; + } + const txs = this.getTransactionsInTree(tree); + for (const tx of txs) { + if (predicate(tx)) { + return true; + } + } + return false; + } + public getReplacedBy(txId: string): string | undefined { return this.replacedBy.get(txId); } diff --git a/frontend/src/app/components/block-overview-graph/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts index 452bb38f5..1b8c88704 100644 --- a/frontend/src/app/components/block-overview-graph/tx-view.ts +++ b/frontend/src/app/components/block-overview-graph/tx-view.ts @@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped { value: number; feerate: number; rate?: number; - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; context?: 'projected' | 'actual'; scene?: BlockScene; @@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped { return auditColors.censored; case 'missing': case 'sigop': - case 'fullrbf': + case 'rbf': return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; case 'fresh': case 'freshcpfp': 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 59450326b..c62779b69 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 @@ -53,7 +53,7 @@ Recently CPFP'd Added Marginal fee rate - Full RBF + Conflicting diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index ce3317255..ec9a49504 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -339,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy { const isSelected = {}; const isFresh = {}; const isSigop = {}; - const isFullRbf = {}; + const isRbf = {}; this.numMissing = 0; this.numUnexpected = 0; @@ -363,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy { isSigop[txid] = true; } for (const txid of blockAudit.fullrbfTxs || []) { - isFullRbf[txid] = true; + isRbf[txid] = true; } // set transaction statuses for (const tx of blockAudit.template) { @@ -381,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy { } } else if (isSigop[tx.txid]) { tx.status = 'sigop'; - } else if (isFullRbf[tx.txid]) { - tx.status = 'fullrbf'; + } else if (isRbf[tx.txid]) { + tx.status = 'rbf'; } else { tx.status = 'missing'; } @@ -398,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy { tx.status = 'added'; } else if (inTemplate[tx.txid]) { tx.status = 'found'; - } else if (isFullRbf[tx.txid]) { - tx.status = 'fullrbf'; + } else if (isRbf[tx.txid]) { + tx.status = 'rbf'; } else { tx.status = 'selected'; isSelected[tx.txid] = true; diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 2b434c44d..4249fd9db 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -174,7 +174,7 @@ export interface TransactionStripped { vsize: number; value: number; rate?: number; // effective fee rate - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; context?: 'projected' | 'actual'; } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 15d97fa8d..e0ecdfeda 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -89,7 +89,7 @@ export interface TransactionStripped { vsize: number; value: number; rate?: number; // effective fee rate - status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf'; + status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf'; context?: 'projected' | 'actual'; } From 07b0f24cf15d2e72a3b0458969278733addcb9dc Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 25 Jul 2023 14:26:43 +0900 Subject: [PATCH 28/30] Update frontend/src/app/shared/pipes/bytes-pipe/utils.ts --- frontend/src/app/shared/pipes/bytes-pipe/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts index 86a1e1a1d..2700be45d 100644 --- a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts +++ b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts @@ -332,6 +332,5 @@ export function hasTouchScreen(): boolean { /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA); } } - console.log(hasTouchScreen); return hasTouchScreen; } \ No newline at end of file From 6d5be78dd06d2b13a6636201eb1d1cb960e50a2f Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Tue, 25 Jul 2023 15:03:39 +0900 Subject: [PATCH 29/30] [search bar] use afterviewinit instead of afterviewchecked --- .../mining-dashboard.component.ts | 17 +++++++++++++---- .../search-form/search-form.component.ts | 8 +++++--- .../src/app/dashboard/dashboard.component.ts | 6 +++--- .../lightning-dashboard.component.ts | 6 +++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index c7670bc1e..6353ab8b8 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,7 +1,8 @@ -import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { SeoService } from '../../services/seo.service'; import { WebsocketService } from '../../services/websocket.service'; import { StateService } from '../../services/state.service'; +import { EventType, NavigationStart, Router } from '@angular/router'; @Component({ selector: 'app-mining-dashboard', @@ -9,11 +10,12 @@ import { StateService } from '../../services/state.service'; styleUrls: ['./mining-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MiningDashboardComponent implements OnInit, AfterViewChecked { +export class MiningDashboardComponent implements OnInit, AfterViewInit { constructor( private seoService: SeoService, private websocketService: WebsocketService, - private stateService: StateService + private stateService: StateService, + private router: Router ) { this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`); } @@ -22,7 +24,14 @@ export class MiningDashboardComponent implements OnInit, AfterViewChecked { this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); } - ngAfterViewChecked(): void { + ngAfterViewInit(): void { this.stateService.focusSearchInputDesktop(); + this.router.events.subscribe((e: NavigationStart) => { + if (e.type === EventType.NavigationStart) { + if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input + this.stateService.focusSearchInputDesktop(); + } + } + }); } } diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 2fc25748e..61b3351b7 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -65,13 +65,15 @@ export class SearchFormComponent implements OnInit { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page - if (e.type === EventType.NavigationStart) { + if (this.searchInput && e.type === EventType.NavigationStart) { this.searchInput.nativeElement.blur(); } }); - this.stateService.searchFocus$.subscribe(focus => { - if (this.searchInput && focus === true) { + this.stateService.searchFocus$.subscribe(() => { + if (!this.searchInput) { // Try again a bit later once the view is properly initialized + setTimeout(() => this.searchInput.nativeElement.focus(), 100); + } else if (this.searchInput) { this.searchInput.nativeElement.focus(); } }); diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index 6d61953cf..05381453d 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -1,4 +1,4 @@ -import { AfterViewChecked, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { combineLatest, merge, Observable, of, Subscription } from 'rxjs'; import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface'; @@ -31,7 +31,7 @@ interface MempoolStatsData { styleUrls: ['./dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked { +export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { featuredAssets$: Observable; network$: Observable; mempoolBlocksData$: Observable; @@ -57,7 +57,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked { private seoService: SeoService ) { } - ngAfterViewChecked(): void { + ngAfterViewInit(): void { this.stateService.focusSearchInputDesktop(); } diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index adaa8d115..e58d5f124 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,4 +1,4 @@ -import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; import { INodesRanking } from '../../interfaces/node-api.interface'; @@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service'; styleUrls: ['./lightning-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class LightningDashboardComponent implements OnInit, AfterViewChecked { +export class LightningDashboardComponent implements OnInit, AfterViewInit { statistics$: Observable; nodesRanking$: Observable; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; @@ -30,7 +30,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewChecked { this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); } - ngAfterViewChecked(): void { + ngAfterViewInit(): void { this.stateService.focusSearchInputDesktop(); } } From e15c0c6c7a62b9d85bd6c0e5d6c02e64d8509d36 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 25 Jul 2023 21:18:19 +0900 Subject: [PATCH 30/30] Fix key navigation subscription leak --- .../mempool-blocks.component.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index 71075b261..cedcf03f4 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -50,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { blockSubscription: Subscription; networkSubscription: Subscription; chainTipSubscription: Subscription; + keySubscription: Subscription; + isTabHiddenSubscription: Subscription; network = ''; now = new Date().getTime(); timeOffset = 0; @@ -116,7 +118,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.calculateTransactionPosition(); }); this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks); - this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); + this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden); this.loadingBlocks$ = combineLatest([ this.stateService.isLoadingWebSocket$, this.stateService.isLoadingMempool$ @@ -224,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.networkSubscription = this.stateService.networkChanged$ .subscribe((network) => this.network = network); - this.stateService.keyNavigation$.subscribe((event) => { + this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => { if (this.markIndex === undefined) { return; } @@ -235,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { if (this.mempoolBlocks[this.markIndex - 1]) { this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]); } else { - this.stateService.blocks$ - .pipe(map((blocks) => blocks[0])) - .subscribe((block) => { - if (this.stateService.latestBlockHeight === block.height) { - this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }}); - } - }); + const blocks = this.stateService.blocksSubject$.getValue(); + for (const block of (blocks || [])) { + if (this.stateService.latestBlockHeight === block.height) { + this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }}); + } + } } } else if (event.key === nextKey) { if (this.mempoolBlocks[this.markIndex + 1]) { @@ -265,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { this.networkSubscription.unsubscribe(); this.timeLtrSubscription.unsubscribe(); this.chainTipSubscription.unsubscribe(); + this.keySubscription.unsubscribe(); + this.isTabHiddenSubscription.unsubscribe(); clearTimeout(this.resetTransitionTimeout); }