diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index d1a2d70a9..703e24f09 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -276,7 +276,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); } diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index f5c940218..b77ce80f1 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -191,15 +191,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-scriptpubkey'] = '41' + matchedAddress + 'ac'; + } else { + client['track-address'] = matchedAddress; + client['track-scriptpubkey'] = null; + } } else { client['track-address'] = null; + client['track-scriptpubkey'] = null; } } @@ -554,6 +561,44 @@ class WebsocketHandler { } } + 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-scriptpubkey']); + 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-scriptpubkey']); + 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[] = []; @@ -845,6 +890,33 @@ class WebsocketHandler { } } + 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-scriptpubkey'])) { + foundTransactions.push(tx); + return; + } + if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) { + 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[] = []; 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; 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 33db897a5..2269d38a9 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -51,6 +51,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; @@ -117,7 +119,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$ @@ -225,7 +227,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; } @@ -236,13 +238,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]) { @@ -266,6 +267,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); }