From e15c0c6c7a62b9d85bd6c0e5d6c02e64d8509d36 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 25 Jul 2023 21:18:19 +0900 Subject: [PATCH 1/6] 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); } From 9b65fbd98c7fac134e68705fa6be558de19699d5 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jul 2023 15:53:52 +0900 Subject: [PATCH 2/6] Show new mined transactions on the address page --- .../components/address/address.component.ts | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index ae1f6dbbe..64d3a4143 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -166,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy { }); this.stateService.mempoolTransactions$ - .subscribe((transaction) => { - if (this.transactions.some((t) => t.txid === transaction.txid)) { - return; - } - - this.transactions.unshift(transaction); - this.transactions = this.transactions.slice(); - this.txCount++; - - if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) { - this.audioService.playSound('cha-ching'); - } else { - this.audioService.playSound('chime'); - } - - transaction.vin.forEach((vin) => { - if (vin.prevout.scriptpubkey_address === this.address.address) { - this.sent += vin.prevout.value; - } - }); - transaction.vout.forEach((vout) => { - if (vout.scriptpubkey_address === this.address.address) { - this.received += vout.value; - } - }); + .subscribe(tx => { + this.addTransaction(tx); }); this.stateService.blockTransactions$ @@ -200,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy { tx.status = transaction.status; this.transactions = this.transactions.slice(); this.audioService.playSound('magic'); + } else { + if (this.addTransaction(transaction, false)) { + this.audioService.playSound('magic'); + } } this.totalConfirmedTxCount++; this.loadedConfirmedTxCount++; }); } + addTransaction(transaction: Transaction, playSound: boolean = true): boolean { + if (this.transactions.some((t) => t.txid === transaction.txid)) { + return false; + } + + this.transactions.unshift(transaction); + this.transactions = this.transactions.slice(); + this.txCount++; + + if (playSound) { + if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) { + this.audioService.playSound('cha-ching'); + } else { + this.audioService.playSound('chime'); + } + } + + transaction.vin.forEach((vin) => { + if (vin?.prevout?.scriptpubkey_address === this.address.address) { + this.sent += vin.prevout.value; + } + }); + transaction.vout.forEach((vout) => { + if (vout?.scriptpubkey_address === this.address.address) { + this.received += vout.value; + } + }); + + return true; + } + loadMore() { if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) { return; From 74b87b60065b617b93cbcec6219a0f2281f26387 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 26 Jul 2023 10:47:59 +0900 Subject: [PATCH 3/6] Support p2pk track-address websocket subscriptions --- backend/src/api/transaction-utils.ts | 9 ++++ backend/src/api/websocket-handler.ts | 76 +++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index 0b10afdfb..b8a9a108a 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface'; import { Common } from './common'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import * as bitcoinjs from 'bitcoinjs-lib'; +import crypto from 'node:crypto'; class TransactionUtils { constructor() { } @@ -170,6 +171,14 @@ class TransactionUtils { 16 ); } + + public calcScriptHash(script: string): string { + if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) { + throw new Error('script is not a valid hex string'); + } + const buf = Buffer.from(script, 'hex'); + return crypto.createHash('sha256').update(buf).digest('hex'); + } } export default new TransactionUtils(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 56c8513cd..3438e0e0c 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -183,15 +183,22 @@ class WebsocketHandler { } if (parsedMessage && parsedMessage['track-address']) { - if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/ + if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[0-9a-fA-F]{130})$/ .test(parsedMessage['track-address'])) { let matchedAddress = parsedMessage['track-address']; if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) { matchedAddress = matchedAddress.toLowerCase(); } - client['track-address'] = matchedAddress; + if (/^[0-9a-fA-F]{130}$/.test(parsedMessage['track-address'])) { + client['track-address'] = null; + client['track-scripthash'] = transactionUtils.calcScriptHash('41' + matchedAddress + 'ac'); + } else { + client['track-address'] = matchedAddress; + client['track-scripthash'] = null; + } } else { client['track-address'] = null; + client['track-scripthash'] = null; } } @@ -546,6 +553,44 @@ class WebsocketHandler { } } + if (client['track-scripthash']) { + const foundTransactions: TransactionExtended[] = []; + + for (const tx of newTransactions) { + const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash']); + if (someVin) { + if (config.MEMPOOL.BACKEND !== 'esplora') { + try { + const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); + foundTransactions.push(fullTx); + } catch (e) { + logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); + } + } else { + foundTransactions.push(tx); + } + return; + } + const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash']); + if (someVout) { + if (config.MEMPOOL.BACKEND !== 'esplora') { + try { + const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true); + foundTransactions.push(fullTx); + } catch (e) { + logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); + } + } else { + foundTransactions.push(tx); + } + } + } + + if (foundTransactions.length) { + response['address-transactions'] = JSON.stringify(foundTransactions); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; @@ -821,6 +866,33 @@ class WebsocketHandler { } } + if (client['track-scripthash']) { + const foundTransactions: TransactionExtended[] = []; + + transactions.forEach((tx) => { + if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash'])) { + foundTransactions.push(tx); + return; + } + if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash'])) { + foundTransactions.push(tx); + } + }); + + if (foundTransactions.length) { + foundTransactions.forEach((tx) => { + tx.status = { + confirmed: true, + block_height: block.height, + block_hash: block.id, + block_time: block.timestamp, + }; + }); + + response['block-transactions'] = JSON.stringify(foundTransactions); + } + } + if (client['track-asset']) { const foundTransactions: TransactionExtended[] = []; From 5b2470955d480656f2f3e636bc257a9da3f7bf0d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jul 2023 16:04:03 +0900 Subject: [PATCH 4/6] track p2pk addresses by scriptpubkey not scripthash --- backend/src/api/websocket-handler.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 3438e0e0c..74c4ed832 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -191,14 +191,14 @@ class WebsocketHandler { } if (/^[0-9a-fA-F]{130}$/.test(parsedMessage['track-address'])) { client['track-address'] = null; - client['track-scripthash'] = transactionUtils.calcScriptHash('41' + matchedAddress + 'ac'); + client['track-scriptpubkey'] = '41' + matchedAddress + 'ac'; } else { client['track-address'] = matchedAddress; - client['track-scripthash'] = null; + client['track-scriptpubkey'] = null; } } else { client['track-address'] = null; - client['track-scripthash'] = null; + client['track-scriptpubkey'] = null; } } @@ -553,11 +553,11 @@ class WebsocketHandler { } } - if (client['track-scripthash']) { + if (client['track-scriptpubkey']) { const foundTransactions: TransactionExtended[] = []; for (const tx of newTransactions) { - const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash']); + const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']); if (someVin) { if (config.MEMPOOL.BACKEND !== 'esplora') { try { @@ -571,7 +571,7 @@ class WebsocketHandler { } return; } - const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash']); + const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']); if (someVout) { if (config.MEMPOOL.BACKEND !== 'esplora') { try { @@ -866,15 +866,15 @@ class WebsocketHandler { } } - if (client['track-scripthash']) { + if (client['track-scriptpubkey']) { const foundTransactions: TransactionExtended[] = []; transactions.forEach((tx) => { - if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash'])) { + if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) { foundTransactions.push(tx); return; } - if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash'])) { + if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) { foundTransactions.push(tx); } }); From 63ccecf4107823194787e0aca8d7309f0dfde9df Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 28 Jul 2023 16:14:28 +0900 Subject: [PATCH 5/6] remove unused calcScriptHash function --- backend/src/api/transaction-utils.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts index b8a9a108a..0b10afdfb 100644 --- a/backend/src/api/transaction-utils.ts +++ b/backend/src/api/transaction-utils.ts @@ -3,7 +3,6 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface'; import { Common } from './common'; import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory'; import * as bitcoinjs from 'bitcoinjs-lib'; -import crypto from 'node:crypto'; class TransactionUtils { constructor() { } @@ -171,14 +170,6 @@ class TransactionUtils { 16 ); } - - public calcScriptHash(script: string): string { - if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) { - throw new Error('script is not a valid hex string'); - } - const buf = Buffer.from(script, 'hex'); - return crypto.createHash('sha256').update(buf).digest('hex'); - } } export default new TransactionUtils(); From b1bdb528512c3276c2351397e29615b4dda71aea Mon Sep 17 00:00:00 2001 From: wiz Date: Fri, 28 Jul 2023 23:39:33 +0900 Subject: [PATCH 6/6] ops: Fix a classic typo in mempool clear protection log print --- backend/src/api/mempool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 945b78738..e822ba329 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -274,7 +274,7 @@ class Mempool { logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`); setTimeout(() => { this.mempoolProtection = 2; - logger.warn('Mempool clear protection resumed.'); + logger.warn('Mempool clear protection ended, normal operation resumed.'); }, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES); }