2020-10-13 15:27:52 +07:00
import logger from '../logger' ;
2020-02-26 17:49:53 +07:00
import * as WebSocket from 'ws' ;
2022-07-04 08:37:36 -07:00
import {
2023-05-29 15:56:29 -04:00
BlockExtended , TransactionExtended , MempoolTransactionExtended , WebsocketResponse ,
2024-03-31 07:55:43 +00:00
OptimizedStatistic , ILoadingIndicators , GbtCandidates , TxTrackingInfo ,
2022-07-04 08:37:36 -07:00
} from '../mempool.interfaces' ;
2020-02-26 17:49:53 +07:00
import blocks from './blocks' ;
import memPool from './mempool' ;
2020-05-27 15:18:04 +07:00
import backendInfo from './backend-info' ;
2020-02-26 17:49:53 +07:00
import mempoolBlocks from './mempool-blocks' ;
2020-06-08 18:55:53 +07:00
import { Common } from './common' ;
2021-01-05 18:57:06 +07:00
import loadingIndicators from './loading-indicators' ;
2021-01-10 17:38:59 +07:00
import config from '../config' ;
import transactionUtils from './transaction-utils' ;
2023-07-14 16:08:57 +09:00
import rbfCache , { ReplacementInfo } from './rbf-cache' ;
2022-03-12 14:55:42 +01:00
import difficultyAdjustment from './difficulty-adjustment' ;
2022-05-27 12:52:40 +02:00
import feeApi from './fee-api' ;
2022-07-06 22:27:45 +02:00
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository' ;
2022-07-07 19:11:42 +02:00
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository' ;
2022-10-19 17:10:45 +00:00
import Audit from './audit' ;
2023-02-15 16:05:14 +09:00
import priceUpdater from '../tasks/price-updater' ;
2023-03-04 10:51:13 +09:00
import { ApiPrice } from '../repositories/PricesRepository' ;
2023-06-03 16:54:12 -04:00
import accelerationApi from './services/acceleration' ;
2023-06-13 17:03:36 -04:00
import mempool from './mempool' ;
2024-02-10 20:45:58 +08:00
import statistics from './statistics/statistics' ;
2024-02-27 18:09:15 +00:00
import accelerationRepository from '../repositories/AccelerationRepository' ;
2024-03-03 20:29:54 +00:00
import bitcoinApi from './bitcoin/bitcoin-api-factory' ;
2020-02-26 17:49:53 +07:00
2023-08-07 10:52:40 +09:00
interface AddressTransactions {
mempool : MempoolTransactionExtended [ ] ,
confirmed : MempoolTransactionExtended [ ] ,
2023-08-25 14:39:43 +09:00
removed : MempoolTransactionExtended [ ] ,
2023-08-07 10:52:40 +09:00
}
2023-02-17 17:54:29 -06:00
import bitcoinSecondClient from './bitcoin/bitcoin-second-client' ;
2024-01-05 22:25:07 +00:00
import { calculateCpfp } from './cpfp' ;
2023-08-07 10:52:40 +09:00
2023-06-12 15:31:47 -04:00
// valid 'want' subscriptions
const wantable = [
'blocks' ,
'mempool-blocks' ,
'live-2h-chart' ,
'stats' ,
2024-03-03 20:29:54 +00:00
'tomahawk' ,
2023-06-12 15:31:47 -04:00
] ;
2020-02-26 17:49:53 +07:00
class WebsocketHandler {
private wss : WebSocket.Server | undefined ;
2020-07-14 21:26:02 +07:00
private extraInitProperties = { } ;
2020-02-26 17:49:53 +07:00
2023-05-01 13:08:29 -06:00
private numClients = 0 ;
private numConnected = 0 ;
private numDisconnected = 0 ;
2023-06-12 15:31:47 -04:00
private socketData : { [ key : string ] : string } = { } ;
2023-05-09 19:48:02 -06:00
private serializedInitData : string = '{}' ;
2024-03-15 05:33:23 +00:00
private lastRbfSummary : ReplacementInfo [ ] | null = null ;
2023-05-09 19:48:02 -06:00
2020-05-27 15:18:04 +07:00
constructor ( ) { }
2020-02-26 17:49:53 +07:00
setWebsocketServer ( wss : WebSocket.Server ) {
this . wss = wss ;
}
2023-06-12 15:31:47 -04:00
setExtraInitData ( property : string , value : any ) {
2020-07-14 21:26:02 +07:00
this . extraInitProperties [ property ] = value ;
2023-06-12 15:31:47 -04:00
this . updateSocketDataFields ( this . extraInitProperties ) ;
2023-05-09 19:48:02 -06:00
}
2023-06-12 15:31:47 -04:00
private updateSocketDataFields ( data : { [ property : string ] : any } ) : void {
2023-05-09 19:48:02 -06:00
for ( const property of Object . keys ( data ) ) {
if ( data [ property ] != null ) {
2023-06-12 15:31:47 -04:00
this . socketData [ property ] = JSON . stringify ( data [ property ] ) ;
2023-05-09 19:48:02 -06:00
} else {
2023-06-12 15:31:47 -04:00
delete this . socketData [ property ] ;
2023-05-09 19:48:02 -06:00
}
}
this . serializedInitData = '{'
2023-06-12 15:31:47 -04:00
+ Object . keys ( this . socketData ) . map ( key = > ` " ${ key } ": ${ this . socketData [ key ] } ` ) . join ( ', ' )
+ '}' ;
2023-05-09 19:48:02 -06:00
}
2023-06-12 15:31:47 -04:00
private updateSocketData ( ) : void {
2023-05-09 19:48:02 -06:00
const _blocks = blocks . getBlocks ( ) . slice ( - config . MEMPOOL . INITIAL_BLOCKS_AMOUNT ) ;
const da = difficultyAdjustment . getDifficultyAdjustment ( ) ;
2023-06-12 15:31:47 -04:00
this . updateSocketDataFields ( {
2023-05-09 19:48:02 -06:00
'mempoolInfo' : memPool . getMempoolInfo ( ) ,
'vBytesPerSecond' : memPool . getVBytesPerSecond ( ) ,
'blocks' : _blocks ,
'conversions' : priceUpdater . getLatestPrices ( ) ,
'mempool-blocks' : mempoolBlocks . getMempoolBlocks ( ) ,
'transactions' : memPool . getLatestTransactions ( ) ,
'backendInfo' : backendInfo . getBackendInfo ( ) ,
'loadingIndicators' : loadingIndicators . getLoadingIndicators ( ) ,
'da' : da ? . previousTime ? da : undefined ,
'fees' : feeApi . getRecommendedFee ( ) ,
} ) ;
}
public getSerializedInitData ( ) : string {
return this . serializedInitData ;
2020-07-14 21:26:02 +07:00
}
2020-02-26 17:49:53 +07:00
setupConnectionHandling() {
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-11-25 09:02:27 +00:00
this . wss . on ( 'connection' , ( client : WebSocket , req ) = > {
2023-05-01 13:08:29 -06:00
this . numConnected ++ ;
2023-11-25 09:02:27 +00:00
client [ 'remoteAddress' ] = req . headers [ 'x-forwarded-for' ] || req . socket ? . remoteAddress || 'unknown' ;
2023-05-01 17:59:48 -06:00
client . on ( 'error' , ( e ) = > {
2023-11-25 09:02:27 +00:00
logger . info ( ` websocket client error from ${ client [ 'remoteAddress' ] } : ` + ( e instanceof Error ? e.message : e ) ) ;
2023-05-01 17:59:48 -06:00
client . close ( ) ;
} ) ;
2023-05-01 13:08:29 -06:00
client . on ( 'close' , ( ) = > {
this . numDisconnected ++ ;
} ) ;
2021-04-27 02:13:48 +04:00
client . on ( 'message' , async ( message : string ) = > {
2020-02-26 17:49:53 +07:00
try {
2020-04-13 02:06:10 +07:00
const parsedMessage : WebsocketResponse = JSON . parse ( message ) ;
const response = { } ;
2020-02-26 17:49:53 +07:00
2023-06-12 15:31:47 -04:00
const wantNow = { } ;
if ( parsedMessage && parsedMessage . action === 'want' && Array . isArray ( parsedMessage . data ) ) {
for ( const sub of wantable ) {
const key = ` want- ${ sub } ` ;
const wants = parsedMessage . data . includes ( sub ) ;
2024-03-03 20:29:54 +00:00
if ( wants && ! client [ key ] ) {
2023-06-12 15:31:47 -04:00
wantNow [ key ] = true ;
}
client [ key ] = wants ;
}
client [ 'wants' ] = true ;
}
// send initial data when a client first starts a subscription
2023-06-12 16:32:04 -04:00
if ( wantNow [ 'want-blocks' ] || ( parsedMessage && parsedMessage [ 'refresh-blocks' ] ) ) {
2023-06-12 15:31:47 -04:00
response [ 'blocks' ] = this . socketData [ 'blocks' ] ;
}
if ( wantNow [ 'want-mempool-blocks' ] ) {
response [ 'mempool-blocks' ] = this . socketData [ 'mempool-blocks' ] ;
}
if ( wantNow [ 'want-stats' ] ) {
response [ 'mempoolInfo' ] = this . socketData [ 'mempoolInfo' ] ;
response [ 'vBytesPerSecond' ] = this . socketData [ 'vBytesPerSecond' ] ;
response [ 'fees' ] = this . socketData [ 'fees' ] ;
response [ 'da' ] = this . socketData [ 'da' ] ;
2020-02-26 17:49:53 +07:00
}
2024-03-04 18:29:50 +00:00
if ( wantNow [ 'want-tomahawk' ] ) {
2024-03-03 20:29:54 +00:00
response [ 'tomahawk' ] = JSON . stringify ( bitcoinApi . getHealthStatus ( ) ) ;
}
2020-02-26 17:49:53 +07:00
if ( parsedMessage && parsedMessage [ 'track-tx' ] ) {
if ( /^[a-fA-F0-9]{64}$/ . test ( parsedMessage [ 'track-tx' ] ) ) {
client [ 'track-tx' ] = parsedMessage [ 'track-tx' ] ;
2023-04-21 08:40:21 +09:00
const trackTxid = client [ 'track-tx' ] ;
2022-03-08 14:49:25 +01:00
// Client is telling the transaction wasn't found
2020-04-13 02:06:10 +07:00
if ( parsedMessage [ 'watch-mempool' ] ) {
2023-04-21 08:40:21 +09:00
const rbfCacheTxid = rbfCache . getReplacedBy ( trackTxid ) ;
2022-12-09 14:35:51 -06:00
if ( rbfCacheTxid ) {
2023-06-12 15:31:47 -04:00
response [ 'txReplaced' ] = JSON . stringify ( {
2022-12-09 14:35:51 -06:00
txid : rbfCacheTxid ,
2023-06-12 15:31:47 -04:00
} ) ;
2022-03-08 14:49:25 +01:00
client [ 'track-tx' ] = null ;
} else {
// It might have appeared before we had the time to start watching for it
2023-04-21 08:40:21 +09:00
const tx = memPool . getMempool ( ) [ trackTxid ] ;
2022-03-08 14:49:25 +01:00
if ( tx ) {
if ( config . MEMPOOL . BACKEND === 'esplora' ) {
2023-06-12 15:31:47 -04:00
response [ 'tx' ] = JSON . stringify ( tx ) ;
2022-03-08 14:49:25 +01:00
} else {
// tx.prevout is missing from transactions when in bitcoind mode
try {
2023-05-29 15:56:29 -04:00
const fullTx = await transactionUtils . $getMempoolTransactionExtended ( tx . txid , true ) ;
2023-06-12 15:31:47 -04:00
response [ 'tx' ] = JSON . stringify ( fullTx ) ;
2022-03-08 14:49:25 +01:00
} catch ( e ) {
logger . debug ( 'Error finding transaction: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
2021-08-09 13:01:29 +03:00
} else {
2021-04-27 02:13:48 +04:00
try {
2023-05-29 15:56:29 -04:00
const fullTx = await transactionUtils . $getMempoolTransactionExtended ( client [ 'track-tx' ] , true ) ;
2023-06-12 15:31:47 -04:00
response [ 'tx' ] = JSON . stringify ( fullTx ) ;
2021-04-27 02:13:48 +04:00
} catch ( e ) {
2022-03-08 14:49:25 +01:00
logger . debug ( 'Error finding transaction. ' + ( e instanceof Error ? e.message : e ) ) ;
client [ 'track-mempool-tx' ] = parsedMessage [ 'track-tx' ] ;
2021-04-27 02:13:48 +04:00
}
}
2020-04-13 02:06:10 +07:00
}
}
2023-04-21 08:40:21 +09:00
const tx = memPool . getMempool ( ) [ trackTxid ] ;
if ( tx && tx . position ) {
2023-07-18 15:05:44 +09:00
const position : { block : number , vsize : number , accelerated? : boolean } = {
2023-06-13 13:35:25 -04:00
. . . tx . position
} ;
if ( tx . acceleration ) {
position . accelerated = tx . acceleration ;
}
2023-06-12 15:31:47 -04:00
response [ 'txPosition' ] = JSON . stringify ( {
2023-04-21 08:40:21 +09:00
txid : trackTxid ,
2023-06-13 13:35:25 -04:00
position
2023-06-12 15:31:47 -04:00
} ) ;
2023-04-21 08:40:21 +09:00
}
2020-02-26 17:49:53 +07:00
} else {
client [ 'track-tx' ] = null ;
}
}
2024-03-31 07:55:43 +00:00
if ( parsedMessage && parsedMessage [ 'track-txs' ] ) {
const txids : string [ ] = [ ] ;
if ( Array . isArray ( parsedMessage [ 'track-txs' ] ) ) {
for ( const txid of parsedMessage [ 'track-txs' ] ) {
if ( /^[a-fA-F0-9]{64}$/ . test ( txid ) ) {
txids . push ( txid ) ;
}
}
}
const txs : { [ txid : string ] : TxTrackingInfo } = { } ;
for ( const txid of txids ) {
const txInfo : TxTrackingInfo = {
confirmed : true ,
} ;
const rbfCacheTxid = rbfCache . getReplacedBy ( txid ) ;
if ( rbfCacheTxid ) {
txInfo . replacedBy = rbfCacheTxid ;
txInfo . confirmed = false ;
}
const tx = memPool . getMempool ( ) [ txid ] ;
if ( tx && tx . position ) {
txInfo . position = {
. . . tx . position
} ;
if ( tx . acceleration ) {
txInfo . accelerated = tx . acceleration ;
}
}
if ( tx ) {
txInfo . confirmed = false ;
}
txs [ txid ] = txInfo ;
}
if ( txids . length ) {
client [ 'track-txs' ] = txids ;
} else {
client [ 'track-txs' ] = null ;
}
if ( Object . keys ( txs ) . length ) {
response [ 'tracked-txs' ] = JSON . stringify ( txs ) ;
}
}
2020-02-26 17:49:53 +07:00
if ( parsedMessage && parsedMessage [ 'track-address' ] ) {
2023-08-10 18:42:10 +09:00
const validAddress = this . testAddress ( parsedMessage [ 'track-address' ] ) ;
if ( validAddress ) {
client [ 'track-address' ] = validAddress ;
2020-02-26 17:49:53 +07:00
} else {
client [ 'track-address' ] = null ;
}
}
2023-08-07 10:52:40 +09:00
if ( parsedMessage && parsedMessage [ 'track-addresses' ] && Array . isArray ( parsedMessage [ 'track-addresses' ] ) ) {
const addressMap : { [ address : string ] : string } = { } ;
for ( const address of parsedMessage [ 'track-addresses' ] ) {
2023-08-10 18:42:10 +09:00
const validAddress = this . testAddress ( address ) ;
if ( validAddress ) {
addressMap [ address ] = validAddress ;
2021-09-05 00:30:24 +04:00
}
2023-08-07 10:52:40 +09:00
}
2024-01-12 22:10:10 +00:00
if ( Object . keys ( addressMap ) . length > config . MEMPOOL . MAX_TRACKED_ADDRESSES ) {
2024-01-23 14:04:15 +00:00
response [ 'track-addresses-error' ] = ` "too many addresses requested, this connection supports tracking a maximum of ${ config . MEMPOOL . MAX_TRACKED_ADDRESSES } addresses" ` ;
2024-01-12 22:10:10 +00:00
client [ 'track-addresses' ] = null ;
} else if ( Object . keys ( addressMap ) . length > 0 ) {
2023-08-07 10:52:40 +09:00
client [ 'track-addresses' ] = addressMap ;
} else {
client [ 'track-addresses' ] = null ;
}
}
2023-08-07 14:13:41 +09:00
if ( parsedMessage && parsedMessage [ 'track-scriptpubkeys' ] && Array . isArray ( parsedMessage [ 'track-scriptpubkeys' ] ) ) {
const spks : string [ ] = [ ] ;
for ( const spk of parsedMessage [ 'track-scriptpubkeys' ] ) {
if ( /^[a-fA-F0-9]+$/ . test ( spk ) ) {
2023-08-10 18:45:17 +09:00
spks . push ( spk . toLowerCase ( ) ) ;
2023-07-26 10:47:59 +09:00
}
2023-08-07 14:13:41 +09:00
}
2024-01-12 22:10:10 +00:00
if ( spks . length > config . MEMPOOL . MAX_TRACKED_ADDRESSES ) {
2024-01-23 14:04:15 +00:00
response [ 'track-scriptpubkeys-error' ] = ` "too many scriptpubkeys requested, this connection supports tracking a maximum of ${ config . MEMPOOL . MAX_TRACKED_ADDRESSES } scriptpubkeys" ` ;
2024-01-12 22:10:10 +00:00
client [ 'track-scriptpubkeys' ] = null ;
} else if ( spks . length ) {
2023-08-07 14:13:41 +09:00
client [ 'track-scriptpubkeys' ] = spks ;
2020-02-26 17:49:53 +07:00
} else {
2023-08-07 14:13:41 +09:00
client [ 'track-scriptpubkeys' ] = null ;
2020-02-26 17:49:53 +07:00
}
}
2020-05-05 15:26:23 +07:00
if ( parsedMessage && parsedMessage [ 'track-asset' ] ) {
if ( /^[a-fA-F0-9]{64}$/ . test ( parsedMessage [ 'track-asset' ] ) ) {
client [ 'track-asset' ] = parsedMessage [ 'track-asset' ] ;
} else {
client [ 'track-asset' ] = null ;
}
}
2022-06-02 01:29:03 +04:00
if ( parsedMessage && parsedMessage [ 'track-mempool-block' ] !== undefined ) {
2022-05-30 17:29:30 +00:00
if ( Number . isInteger ( parsedMessage [ 'track-mempool-block' ] ) && parsedMessage [ 'track-mempool-block' ] >= 0 ) {
const index = parsedMessage [ 'track-mempool-block' ] ;
client [ 'track-mempool-block' ] = index ;
const mBlocksWithTransactions = mempoolBlocks . getMempoolBlocksWithTransactions ( ) ;
2023-06-12 15:31:47 -04:00
response [ 'projected-block-transactions' ] = JSON . stringify ( {
2022-06-06 14:31:17 +04:00
index : index ,
2024-02-08 22:40:22 +00:00
blockTransactions : ( mBlocksWithTransactions [ index ] ? . transactions || [ ] ) . map ( mempoolBlocks . compressTx ) ,
2023-06-12 15:31:47 -04:00
} ) ;
2022-05-30 17:29:30 +00:00
} else {
client [ 'track-mempool-block' ] = null ;
}
}
2022-12-14 16:51:53 -06:00
if ( parsedMessage && parsedMessage [ 'track-rbf' ] !== undefined ) {
if ( [ 'all' , 'fullRbf' ] . includes ( parsedMessage [ 'track-rbf' ] ) ) {
client [ 'track-rbf' ] = parsedMessage [ 'track-rbf' ] ;
2023-06-12 15:31:47 -04:00
response [ 'rbfLatest' ] = JSON . stringify ( rbfCache . getRbfTrees ( parsedMessage [ 'track-rbf' ] === 'fullRbf' ) ) ;
2022-12-14 16:51:53 -06:00
} else {
client [ 'track-rbf' ] = false ;
}
}
2023-07-14 16:08:57 +09:00
if ( parsedMessage && parsedMessage [ 'track-rbf-summary' ] != null ) {
if ( parsedMessage [ 'track-rbf-summary' ] ) {
client [ 'track-rbf-summary' ] = true ;
2023-07-17 17:53:26 +09:00
if ( this . socketData [ 'rbfSummary' ] != null ) {
response [ 'rbfLatestSummary' ] = this . socketData [ 'rbfSummary' ] ;
}
2023-07-14 16:08:57 +09:00
} else {
client [ 'track-rbf-summary' ] = false ;
}
}
2020-02-26 17:49:53 +07:00
if ( parsedMessage . action === 'init' ) {
2023-07-16 18:39:51 +09:00
if ( ! this . socketData [ 'blocks' ] ? . length || ! this . socketData [ 'da' ] || ! this . socketData [ 'backendInfo' ] || ! this . socketData [ 'conversions' ] ) {
2023-06-12 15:31:47 -04:00
this . updateSocketData ( ) ;
2023-05-09 19:48:02 -06:00
}
2023-06-12 15:31:47 -04:00
if ( ! this . socketData [ 'blocks' ] ? . length ) {
2020-02-26 17:49:53 +07:00
return ;
}
2023-05-09 19:48:02 -06:00
client . send ( this . serializedInitData ) ;
2020-02-26 17:49:53 +07:00
}
2020-03-06 02:05:26 +07:00
if ( parsedMessage . action === 'ping' ) {
2023-06-12 15:31:47 -04:00
response [ 'pong' ] = JSON . stringify ( true ) ;
2020-04-13 02:06:10 +07:00
}
2020-10-07 20:15:42 +07:00
if ( parsedMessage [ 'track-donation' ] && parsedMessage [ 'track-donation' ] . length === 22 ) {
client [ 'track-donation' ] = parsedMessage [ 'track-donation' ] ;
}
2021-03-05 15:38:46 +07:00
if ( parsedMessage [ 'track-bisq-market' ] ) {
if ( /^[a-z]{3}_[a-z]{3}$/ . test ( parsedMessage [ 'track-bisq-market' ] ) ) {
client [ 'track-bisq-market' ] = parsedMessage [ 'track-bisq-market' ] ;
} else {
client [ 'track-bisq-market' ] = null ;
}
}
2020-04-13 02:06:10 +07:00
if ( Object . keys ( response ) . length ) {
2023-12-31 18:05:25 +00:00
client . send ( this . serializeResponse ( response ) ) ;
2020-03-06 02:05:26 +07:00
}
2020-02-26 17:49:53 +07:00
} catch ( e ) {
2023-11-25 09:02:27 +00:00
logger . debug ( ` Error parsing websocket message from ${ client [ 'remoteAddress' ] } : ` + ( e instanceof Error ? e.message : e ) ) ;
2023-05-01 17:59:48 -06:00
client . close ( ) ;
2020-02-26 17:49:53 +07:00
}
} ) ;
} ) ;
}
2020-10-07 20:15:42 +07:00
handleNewDonation ( id : string ) {
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2022-07-04 08:37:36 -07:00
this . wss . clients . forEach ( ( client ) = > {
2020-10-07 20:15:42 +07:00
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
if ( client [ 'track-donation' ] === id ) {
client . send ( JSON . stringify ( { donationConfirmed : true } ) ) ;
}
} ) ;
}
2021-01-05 18:57:06 +07:00
handleLoadingChanged ( indicators : ILoadingIndicators ) {
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-06-12 15:31:47 -04:00
this . updateSocketDataFields ( { 'loadingIndicators' : indicators } ) ;
2023-05-09 19:48:02 -06:00
2023-05-09 17:44:38 -06:00
const response = JSON . stringify ( { loadingIndicators : indicators } ) ;
2022-07-04 08:37:36 -07:00
this . wss . clients . forEach ( ( client ) = > {
2021-01-05 18:57:06 +07:00
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
2023-05-09 17:44:38 -06:00
client . send ( response ) ;
2021-01-05 18:57:06 +07:00
} ) ;
}
2023-03-04 10:51:13 +09:00
handleNewConversionRates ( conversionRates : ApiPrice ) {
2021-01-06 23:31:33 +07:00
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-06-12 15:31:47 -04:00
this . updateSocketDataFields ( { 'conversions' : conversionRates } ) ;
2023-05-09 19:48:02 -06:00
2023-05-09 17:44:38 -06:00
const response = JSON . stringify ( { conversions : conversionRates } ) ;
2022-07-04 08:37:36 -07:00
this . wss . clients . forEach ( ( client ) = > {
2021-01-06 23:31:33 +07:00
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
2023-05-09 17:44:38 -06:00
client . send ( response ) ;
2021-01-06 23:31:33 +07:00
} ) ;
}
2020-06-09 02:08:46 +07:00
handleNewStatistic ( stats : OptimizedStatistic ) {
2020-02-26 17:49:53 +07:00
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-05-01 13:08:29 -06:00
this . printLogs ( ) ;
2023-05-09 17:44:38 -06:00
const response = JSON . stringify ( {
'live-2h-chart' : stats
} ) ;
2022-07-04 08:37:36 -07:00
this . wss . clients . forEach ( ( client ) = > {
2020-02-26 17:49:53 +07:00
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
if ( ! client [ 'want-live-2h-chart' ] ) {
return ;
}
2023-05-09 17:44:38 -06:00
client . send ( response ) ;
2020-02-26 17:49:53 +07:00
} ) ;
}
2023-07-08 01:07:06 -04:00
handleReorg ( ) : void {
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
const da = difficultyAdjustment . getDifficultyAdjustment ( ) ;
// update init data
this . updateSocketDataFields ( {
'blocks' : blocks . getBlocks ( ) ,
'da' : da ? . previousTime ? da : undefined ,
} ) ;
this . wss . clients . forEach ( ( client ) = > {
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
const response = { } ;
if ( client [ 'want-blocks' ] ) {
response [ 'blocks' ] = this . socketData [ 'blocks' ] ;
}
if ( client [ 'want-stats' ] ) {
response [ 'da' ] = this . socketData [ 'da' ] ;
}
if ( Object . keys ( response ) . length ) {
2023-12-31 18:05:25 +00:00
client . send ( this . serializeResponse ( response ) ) ;
2023-07-08 01:07:06 -04:00
}
} ) ;
}
2023-06-28 19:51:52 -04:00
async $handleMempoolChange ( newMempool : { [ txid : string ] : MempoolTransactionExtended } , mempoolSize : number ,
2023-02-17 17:54:29 -06:00
newTransactions : MempoolTransactionExtended [ ] , deletedTransactions : MempoolTransactionExtended [ ] , accelerationDelta : string [ ] ,
candidates? : GbtCandidates ) : Promise < void > {
2020-02-26 17:49:53 +07:00
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-05-01 13:08:29 -06:00
this . printLogs ( ) ;
2023-02-17 17:54:29 -06:00
const transactionIds = ( memPool . limitGBT && candidates ) ? Object . keys ( candidates ? . txs || { } ) : Object . keys ( newMempool ) ;
let added = newTransactions ;
let removed = deletedTransactions ;
if ( memPool . limitGBT ) {
added = candidates ? . added || [ ] ;
removed = candidates ? . removed || [ ] ;
}
2024-01-06 18:26:03 +00:00
if ( config . MEMPOOL . RUST_GBT ) {
await mempoolBlocks . $rustUpdateBlockTemplates ( transactionIds , newMempool , added , removed , candidates , config . MEMPOOL_SERVICES . ACCELERATIONS ) ;
2022-11-30 17:56:53 +09:00
} else {
2024-01-06 18:26:03 +00:00
await mempoolBlocks . $updateBlockTemplates ( transactionIds , newMempool , added , removed , candidates , accelerationDelta , true , config . MEMPOOL_SERVICES . ACCELERATIONS ) ;
2022-11-16 18:18:59 -06:00
}
2022-11-30 17:56:53 +09:00
2022-11-20 16:12:39 +09:00
const mBlocks = mempoolBlocks . getMempoolBlocks ( ) ;
const mBlockDeltas = mempoolBlocks . getMempoolBlockDeltas ( ) ;
2020-02-26 17:49:53 +07:00
const mempoolInfo = memPool . getMempoolInfo ( ) ;
const vBytesPerSecond = memPool . getVBytesPerSecond ( ) ;
2020-06-08 18:55:53 +07:00
const rbfTransactions = Common . findRbfTransactions ( newTransactions , deletedTransactions ) ;
2022-03-12 14:55:42 +01:00
const da = difficultyAdjustment . getDifficultyAdjustment ( ) ;
2022-03-08 14:49:25 +01:00
memPool . handleRbfTransactions ( rbfTransactions ) ;
2022-12-14 08:49:35 -06:00
const rbfChanges = rbfCache . getRbfChanges ( ) ;
2022-12-14 16:51:53 -06:00
let rbfReplacements ;
let fullRbfReplacements ;
2023-07-14 16:08:57 +09:00
let rbfSummary ;
2024-03-15 05:33:23 +00:00
if ( Object . keys ( rbfChanges . trees ) . length || ! this . lastRbfSummary ) {
2022-12-17 09:39:06 -06:00
rbfReplacements = rbfCache . getRbfTrees ( false ) ;
fullRbfReplacements = rbfCache . getRbfTrees ( true ) ;
2024-03-15 05:33:23 +00:00
rbfSummary = rbfCache . getLatestRbfSummary ( ) || [ ] ;
this . lastRbfSummary = rbfSummary ;
2022-12-14 16:51:53 -06:00
}
2023-07-14 16:08:57 +09:00
2023-05-04 19:10:53 -04:00
for ( const deletedTx of deletedTransactions ) {
rbfCache . evict ( deletedTx . txid ) ;
}
2023-05-18 09:51:41 -04:00
memPool . removeFromSpendMap ( deletedTransactions ) ;
memPool . addToSpendMap ( newTransactions ) ;
2022-05-27 12:52:40 +02:00
const recommendedFees = feeApi . getRecommendedFee ( ) ;
2021-03-21 06:06:03 +07:00
2023-07-17 11:02:28 +09:00
const latestTransactions = memPool . getLatestTransactions ( ) ;
2023-06-12 15:31:47 -04:00
2023-05-09 19:48:02 -06:00
// update init data
2023-07-14 16:08:57 +09:00
const socketDataFields = {
2023-06-12 15:31:47 -04:00
'mempoolInfo' : mempoolInfo ,
'vBytesPerSecond' : vBytesPerSecond ,
'mempool-blocks' : mBlocks ,
'transactions' : latestTransactions ,
'loadingIndicators' : loadingIndicators . getLoadingIndicators ( ) ,
'da' : da ? . previousTime ? da : undefined ,
'fees' : recommendedFees ,
2023-07-14 16:08:57 +09:00
} ;
if ( rbfSummary ) {
socketDataFields [ 'rbfSummary' ] = rbfSummary ;
}
this . updateSocketDataFields ( socketDataFields ) ;
2023-05-09 19:48:02 -06:00
2023-05-09 17:44:38 -06:00
// cache serialized objects to avoid stringify-ing the same thing for every client
2023-06-12 15:31:47 -04:00
const responseCache = { . . . this . socketData } ;
2023-05-09 17:44:38 -06:00
function getCachedResponse ( key : string , data ) : string {
if ( ! responseCache [ key ] ) {
responseCache [ key ] = JSON . stringify ( data ) ;
}
return responseCache [ key ] ;
}
// pre-compute new tracked outspends
const outspendCache : { [ txid : string ] : { [ vout : number ] : { vin : number , txid : string } } } = { } ;
const trackedTxs = new Set < string > ( ) ;
this . wss . clients . forEach ( ( client ) = > {
if ( client [ 'track-tx' ] ) {
trackedTxs . add ( client [ 'track-tx' ] ) ;
}
2024-03-31 07:55:43 +00:00
if ( client [ 'track-txs' ] ) {
for ( const txid of client [ 'track-txs' ] ) {
trackedTxs . add ( client [ 'track-tx' ] ) ;
}
}
2023-05-09 17:44:38 -06:00
} ) ;
if ( trackedTxs . size > 0 ) {
for ( const tx of newTransactions ) {
for ( let i = 0 ; i < tx . vin . length ; i ++ ) {
const vin = tx . vin [ i ] ;
if ( trackedTxs . has ( vin . txid ) ) {
if ( ! outspendCache [ vin . txid ] ) {
outspendCache [ vin . txid ] = { [ vin . vout ] : { vin : i , txid : tx.txid } } ;
} else {
outspendCache [ vin . txid ] [ vin . vout ] = { vin : i , txid : tx.txid } ;
}
}
}
}
}
2023-08-04 10:52:15 +09:00
// pre-compute address transactions
const addressCache = this . makeAddressCache ( newTransactions ) ;
2023-08-25 00:57:47 +09:00
const removedAddressCache = this . makeAddressCache ( deletedTransactions ) ;
2023-08-04 10:52:15 +09:00
2022-07-04 08:37:36 -07:00
this . wss . clients . forEach ( async ( client ) = > {
2020-02-26 17:49:53 +07:00
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
const response = { } ;
if ( client [ 'want-stats' ] ) {
2023-05-09 17:44:38 -06:00
response [ 'mempoolInfo' ] = getCachedResponse ( 'mempoolInfo' , mempoolInfo ) ;
response [ 'vBytesPerSecond' ] = getCachedResponse ( 'vBytesPerSecond' , vBytesPerSecond ) ;
2023-05-09 19:48:02 -06:00
response [ 'transactions' ] = getCachedResponse ( 'transactions' , latestTransactions ) ;
2023-03-26 09:05:41 +09:00
if ( da ? . previousTime ) {
2023-05-09 17:44:38 -06:00
response [ 'da' ] = getCachedResponse ( 'da' , da ) ;
2023-03-26 09:05:41 +09:00
}
2023-05-09 17:44:38 -06:00
response [ 'fees' ] = getCachedResponse ( 'fees' , recommendedFees ) ;
2020-02-26 17:49:53 +07:00
}
2022-11-20 16:12:39 +09:00
if ( client [ 'want-mempool-blocks' ] ) {
2023-05-09 17:44:38 -06:00
response [ 'mempool-blocks' ] = getCachedResponse ( 'mempool-blocks' , mBlocks ) ;
2020-02-26 17:49:53 +07:00
}
2024-03-04 18:29:50 +00:00
if ( client [ 'want-tomahawk' ] ) {
2024-03-03 20:29:54 +00:00
response [ 'tomahawk' ] = getCachedResponse ( 'tomahawk' , bitcoinApi . getHealthStatus ( ) ) ;
}
2020-04-13 02:06:10 +07:00
if ( client [ 'track-mempool-tx' ] ) {
const tx = newTransactions . find ( ( t ) = > t . txid === client [ 'track-mempool-tx' ] ) ;
2020-04-13 01:26:53 +07:00
if ( tx ) {
2021-01-10 17:38:59 +07:00
if ( config . MEMPOOL . BACKEND !== 'esplora' ) {
2021-01-24 02:51:22 +07:00
try {
2023-05-29 15:56:29 -04:00
const fullTx = await transactionUtils . $getMempoolTransactionExtended ( tx . txid , true ) ;
2023-05-09 17:44:38 -06:00
response [ 'tx' ] = JSON . stringify ( fullTx ) ;
2021-01-24 02:51:22 +07:00
} catch ( e ) {
2021-08-31 15:09:33 +03:00
logger . debug ( 'Error finding transaction in mempool: ' + ( e instanceof Error ? e.message : e ) ) ;
2021-01-10 17:38:59 +07:00
}
} else {
2023-05-09 17:44:38 -06:00
response [ 'tx' ] = JSON . stringify ( tx ) ;
2021-01-10 17:38:59 +07:00
}
2020-04-13 02:06:10 +07:00
client [ 'track-mempool-tx' ] = null ;
2020-04-13 01:26:53 +07:00
}
}
2020-05-20 17:00:50 +07:00
if ( client [ 'track-address' ] ) {
2023-08-25 00:57:47 +09:00
const newTransactions = Array . from ( addressCache [ client [ 'track-address' ] ] ? . values ( ) || [ ] ) ;
const removedTransactions = Array . from ( removedAddressCache [ client [ 'track-address' ] ] ? . values ( ) || [ ] ) ;
2023-08-04 10:52:15 +09:00
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
2023-08-25 00:57:47 +09:00
const fullTransactions = ( config . MEMPOOL . BACKEND !== 'esplora' ) ? await this . getFullTransactions ( newTransactions ) : newTransactions ;
2020-05-20 17:00:50 +07:00
2023-08-25 00:57:47 +09:00
if ( removedTransactions . length ) {
response [ 'address-removed-transactions' ] = JSON . stringify ( removedTransactions ) ;
}
2023-08-04 10:52:15 +09:00
if ( fullTransactions . length ) {
response [ 'address-transactions' ] = JSON . stringify ( fullTransactions ) ;
2023-07-26 10:47:59 +09:00
}
}
2023-08-07 10:52:40 +09:00
if ( client [ 'track-addresses' ] ) {
const addressMap : { [ address : string ] : AddressTransactions } = { } ;
for ( const [ address , key ] of Object . entries ( client [ 'track-addresses' ] || { } ) ) {
2023-08-25 14:39:43 +09:00
const newTransactions = Array . from ( addressCache [ key as string ] ? . values ( ) || [ ] ) ;
const removedTransactions = Array . from ( removedAddressCache [ key as string ] ? . values ( ) || [ ] ) ;
2023-08-07 10:52:40 +09:00
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
2023-08-25 14:39:43 +09:00
const fullTransactions = ( config . MEMPOOL . BACKEND !== 'esplora' ) ? await this . getFullTransactions ( newTransactions ) : newTransactions ;
2023-08-07 10:52:40 +09:00
if ( fullTransactions ? . length ) {
addressMap [ address ] = {
mempool : fullTransactions ,
confirmed : [ ] ,
2023-08-25 14:39:43 +09:00
removed : removedTransactions ,
2023-08-07 10:52:40 +09:00
} ;
}
}
if ( Object . keys ( addressMap ) . length > 0 ) {
response [ 'multi-address-transactions' ] = JSON . stringify ( addressMap ) ;
}
}
2023-08-07 14:13:41 +09:00
if ( client [ 'track-scriptpubkeys' ] ) {
const spkMap : { [ spk : string ] : AddressTransactions } = { } ;
for ( const spk of client [ 'track-scriptpubkeys' ] || [ ] ) {
2023-08-25 14:39:43 +09:00
const newTransactions = Array . from ( addressCache [ spk as string ] ? . values ( ) || [ ] ) ;
const removedTransactions = Array . from ( removedAddressCache [ spk as string ] ? . values ( ) || [ ] ) ;
2023-08-07 14:13:41 +09:00
// txs may be missing prevouts in non-esplora backends
// so fetch the full transactions now
2023-08-25 14:39:43 +09:00
const fullTransactions = ( config . MEMPOOL . BACKEND !== 'esplora' ) ? await this . getFullTransactions ( newTransactions ) : newTransactions ;
2023-08-07 14:13:41 +09:00
if ( fullTransactions ? . length ) {
spkMap [ spk ] = {
mempool : fullTransactions ,
confirmed : [ ] ,
2023-08-25 14:39:43 +09:00
removed : removedTransactions ,
2023-08-07 14:13:41 +09:00
} ;
}
}
if ( Object . keys ( spkMap ) . length > 0 ) {
response [ 'multi-scriptpubkey-transactions' ] = JSON . stringify ( spkMap ) ;
}
}
2020-05-05 15:26:23 +07:00
if ( client [ 'track-asset' ] ) {
2020-02-26 17:49:53 +07:00
const foundTransactions : TransactionExtended [ ] = [ ] ;
newTransactions . forEach ( ( tx ) = > {
2021-09-18 13:37:25 +04:00
if ( client [ 'track-asset' ] === Common . nativeAssetId ) {
2020-05-10 01:34:28 +07:00
if ( tx . vin . some ( ( vin ) = > ! ! vin . is_pegin ) ) {
foundTransactions . push ( tx ) ;
return ;
}
if ( tx . vout . some ( ( vout ) = > ! ! vout . pegout ) ) {
foundTransactions . push ( tx ) ;
}
} else {
if ( tx . vin . some ( ( vin ) = > ! ! vin . issuance && vin . issuance . asset_id === client [ 'track-asset' ] ) ) {
foundTransactions . push ( tx ) ;
return ;
}
if ( tx . vout . some ( ( vout ) = > ! ! vout . asset && vout . asset === client [ 'track-asset' ] ) ) {
foundTransactions . push ( tx ) ;
}
2020-02-26 17:49:53 +07:00
}
} ) ;
if ( foundTransactions . length ) {
2023-05-09 17:44:38 -06:00
response [ 'address-transactions' ] = JSON . stringify ( foundTransactions ) ;
2020-02-26 17:49:53 +07:00
}
}
2022-03-06 18:27:13 +01:00
if ( client [ 'track-tx' ] ) {
2023-04-21 08:40:21 +09:00
const trackTxid = client [ 'track-tx' ] ;
2023-05-09 17:44:38 -06:00
const outspends = outspendCache [ trackTxid ] ;
2022-03-07 19:45:09 +01:00
2023-05-09 17:44:38 -06:00
if ( outspends && Object . keys ( outspends ) . length ) {
response [ 'utxoSpent' ] = JSON . stringify ( outspends ) ;
2022-03-06 18:27:13 +01:00
}
2023-09-29 00:02:01 +01:00
const rbfReplacedBy = rbfChanges . map [ client [ 'track-tx' ] ] ? rbfCache . getReplacedBy ( client [ 'track-tx' ] ) : false ;
2022-12-17 09:39:06 -06:00
if ( rbfReplacedBy ) {
2023-05-09 17:44:38 -06:00
response [ 'rbfTransaction' ] = JSON . stringify ( {
2022-12-17 09:39:06 -06:00
txid : rbfReplacedBy ,
2023-06-12 15:31:47 -04:00
} ) ;
2020-06-08 18:55:53 +07:00
}
2022-12-14 08:49:35 -06:00
const rbfChange = rbfChanges . map [ client [ 'track-tx' ] ] ;
if ( rbfChange ) {
2023-05-09 17:44:38 -06:00
response [ 'rbfInfo' ] = JSON . stringify ( rbfChanges . trees [ rbfChange ] ) ;
2022-12-14 08:49:35 -06:00
}
2023-04-21 08:40:21 +09:00
const mempoolTx = newMempool [ trackTxid ] ;
if ( mempoolTx && mempoolTx . position ) {
2023-08-27 00:30:33 +09:00
const positionData = {
2023-04-21 08:40:21 +09:00
txid : trackTxid ,
2023-06-13 13:35:25 -04:00
position : {
. . . mempoolTx . position ,
accelerated : mempoolTx.acceleration || undefined ,
}
2023-08-27 00:30:33 +09:00
} ;
2024-01-05 22:25:07 +00:00
if ( ! mempoolTx . cpfpChecked ) {
calculateCpfp ( mempoolTx , newMempool ) ;
}
2023-08-27 00:30:33 +09:00
if ( mempoolTx . cpfpDirty ) {
positionData [ 'cpfp' ] = {
ancestors : mempoolTx.ancestors ,
bestDescendant : mempoolTx.bestDescendant || null ,
descendants : mempoolTx.descendants || null ,
effectiveFeePerVsize : mempoolTx.effectiveFeePerVsize || null ,
sigops : mempoolTx.sigops ,
adjustedVsize : mempoolTx.adjustedVsize ,
acceleration : mempoolTx.acceleration
} ;
}
response [ 'txPosition' ] = JSON . stringify ( positionData ) ;
2023-04-21 08:40:21 +09:00
}
2020-06-08 18:55:53 +07:00
}
2024-03-31 07:55:43 +00:00
if ( client [ 'track-txs' ] ) {
const txids = client [ 'track-txs' ] ;
const txs : { [ txid : string ] : TxTrackingInfo } = { } ;
for ( const txid of txids ) {
const txInfo : TxTrackingInfo = { } ;
const outspends = outspendCache [ txid ] ;
if ( outspends && Object . keys ( outspends ) . length ) {
txInfo . utxoSpent = outspends ;
}
const replacedBy = rbfChanges . map [ txid ] ? rbfCache . getReplacedBy ( txid ) : false ;
if ( replacedBy ) {
txInfo . replacedBy = replacedBy ;
}
const mempoolTx = newMempool [ txid ] ;
if ( mempoolTx && mempoolTx . position ) {
txInfo . position = {
. . . mempoolTx . position ,
accelerated : mempoolTx.acceleration || undefined ,
} ;
if ( ! mempoolTx . cpfpChecked ) {
calculateCpfp ( mempoolTx , newMempool ) ;
}
if ( mempoolTx . cpfpDirty ) {
txInfo . cpfp = {
ancestors : mempoolTx.ancestors ,
bestDescendant : mempoolTx.bestDescendant || null ,
descendants : mempoolTx.descendants || null ,
effectiveFeePerVsize : mempoolTx.effectiveFeePerVsize || null ,
sigops : mempoolTx.sigops ,
adjustedVsize : mempoolTx.adjustedVsize ,
} ;
}
}
txs [ txid ] = txInfo ;
}
if ( Object . keys ( txs ) . length ) {
response [ 'tracked-txs' ] = JSON . stringify ( txs ) ;
}
}
2023-07-24 17:22:38 +09:00
if ( client [ 'track-mempool-block' ] >= 0 && memPool . isInSync ( ) ) {
2022-05-30 17:29:30 +00:00
const index = client [ 'track-mempool-block' ] ;
2022-05-31 21:36:42 +00:00
if ( mBlockDeltas [ index ] ) {
2023-05-09 17:44:38 -06:00
response [ 'projected-block-transactions' ] = getCachedResponse ( ` projected-block-transactions- ${ index } ` , {
2022-05-30 17:29:30 +00:00
index : index ,
2022-05-31 21:36:42 +00:00
delta : mBlockDeltas [ index ] ,
2023-05-09 17:44:38 -06:00
} ) ;
2022-05-30 17:29:30 +00:00
}
}
2022-12-14 16:51:53 -06:00
if ( client [ 'track-rbf' ] === 'all' && rbfReplacements ) {
2023-05-09 17:44:38 -06:00
response [ 'rbfLatest' ] = getCachedResponse ( 'rbfLatest' , rbfReplacements ) ;
2022-12-14 16:51:53 -06:00
} else if ( client [ 'track-rbf' ] === 'fullRbf' && fullRbfReplacements ) {
2023-05-09 17:44:38 -06:00
response [ 'rbfLatest' ] = getCachedResponse ( 'fullrbfLatest' , fullRbfReplacements ) ;
2022-12-14 16:51:53 -06:00
}
2023-07-14 16:08:57 +09:00
if ( client [ 'track-rbf-summary' ] && rbfSummary ) {
response [ 'rbfLatestSummary' ] = getCachedResponse ( 'rbfLatestSummary' , rbfSummary ) ;
}
2020-02-26 17:49:53 +07:00
if ( Object . keys ( response ) . length ) {
2023-12-31 18:05:25 +00:00
client . send ( this . serializeResponse ( response ) ) ;
2020-02-26 17:49:53 +07:00
}
} ) ;
}
2022-11-20 16:12:39 +09:00
2023-06-25 20:37:42 -04:00
async handleNewBlock ( block : BlockExtended , txIds : string [ ] , transactions : MempoolTransactionExtended [ ] ) : Promise < void > {
2022-11-16 18:18:59 -06:00
if ( ! this . wss ) {
throw new Error ( 'WebSocket.Server is not set' ) ;
}
2023-05-01 13:08:29 -06:00
this . printLogs ( ) ;
2024-02-10 20:45:58 +08:00
await statistics . runStatistics ( ) ;
2023-05-01 13:08:29 -06:00
2022-11-16 18:18:59 -06:00
const _memPool = memPool . getMempool ( ) ;
2023-02-17 17:54:29 -06:00
const candidateTxs = await memPool . getMempoolCandidates ( ) ;
let candidates : GbtCandidates | undefined = ( memPool . limitGBT && candidateTxs ) ? { txs : candidateTxs , added : [ ] , removed : [ ] } : undefined ;
2024-01-07 17:57:36 +00:00
let transactionIds : string [ ] = ( memPool . limitGBT ) ? Object . keys ( candidates ? . txs || { } ) : Object . keys ( _memPool ) ;
2022-11-16 18:18:59 -06:00
2024-03-11 21:19:03 +00:00
const accelerations = Object . values ( mempool . getAccelerations ( ) ) ;
await accelerationRepository . $indexAccelerationsForBlock ( block , accelerations , transactions ) ;
2024-02-27 18:09:15 +00:00
2023-06-19 18:14:09 -04:00
const rbfTransactions = Common . findMinedRbfTransactions ( transactions , memPool . getSpendMap ( ) ) ;
memPool . handleMinedRbfTransactions ( rbfTransactions ) ;
memPool . removeFromSpendMap ( transactions ) ;
2023-07-24 16:19:19 +09:00
if ( config . MEMPOOL . AUDIT && memPool . isInSync ( ) ) {
2023-02-12 21:43:12 -06:00
let projectedBlocks ;
2024-01-06 18:26:03 +00:00
const auditMempool = _memPool ;
const isAccelerated = config . MEMPOOL_SERVICES . ACCELERATIONS && accelerationApi . isAcceleratedBlock ( block , Object . values ( mempool . getAccelerations ( ) ) ) ;
if ( ( config . MEMPOOL_SERVICES . ACCELERATIONS ) ) {
if ( config . MEMPOOL . RUST_GBT ) {
const added = memPool . limitGBT ? ( candidates ? . added || [ ] ) : [ ] ;
const removed = memPool . limitGBT ? ( candidates ? . removed || [ ] ) : [ ] ;
projectedBlocks = await mempoolBlocks . $rustUpdateBlockTemplates ( transactionIds , auditMempool , added , removed , candidates , isAccelerated , block . extras . pool . id ) ;
2023-05-02 16:57:10 -06:00
} else {
2024-01-06 18:26:03 +00:00
projectedBlocks = await mempoolBlocks . $makeBlockTemplates ( transactionIds , auditMempool , candidates , false , isAccelerated , block . extras . pool . id ) ;
2023-05-02 16:57:10 -06:00
}
2023-02-12 21:43:12 -06:00
} else {
2024-01-06 18:26:03 +00:00
projectedBlocks = mempoolBlocks . getMempoolBlocksWithTransactions ( ) ;
2023-02-12 21:43:12 -06:00
}
2020-02-26 17:49:53 +07:00
2023-07-24 16:19:19 +09:00
if ( Common . indexingEnabled ( ) ) {
2023-05-26 21:10:32 -04:00
const { censored , added , fresh , sigop , fullrbf , accelerated , score , similarity } = Audit . auditBlock ( transactions , projectedBlocks , auditMempool ) ;
2023-02-12 21:43:12 -06:00
const matchRate = Math . round ( score * 100 * 100 ) / 100 ;
2020-06-08 02:08:51 +07:00
2023-06-29 19:24:19 -04:00
const stripped = projectedBlocks [ 0 ] ? . transactions ? projectedBlocks [ 0 ] . transactions : [ ] ;
2022-07-07 19:11:42 +02:00
2023-06-09 13:46:14 -04:00
let totalFees = 0 ;
let totalWeight = 0 ;
for ( const tx of stripped ) {
totalFees += tx . fee ;
totalWeight += ( tx . vsize * 4 ) ;
}
2023-06-05 13:20:46 +02:00
2023-02-12 21:43:12 -06:00
BlocksSummariesRepository . $saveTemplate ( {
height : block.height ,
template : {
id : block.id ,
2023-06-05 13:20:46 +02:00
transactions : stripped ,
2024-01-23 00:44:34 +00:00
} ,
version : 1 ,
2023-02-12 21:43:12 -06:00
} ) ;
BlocksAuditsRepository . $saveAudit ( {
time : block.timestamp ,
height : block.height ,
hash : block.id ,
addedTxs : added ,
missingTxs : censored ,
freshTxs : fresh ,
2023-05-17 11:46:50 -04:00
sigopTxs : sigop ,
2023-06-19 18:14:09 -04:00
fullrbfTxs : fullrbf ,
2023-05-22 18:16:58 -04:00
acceleratedTxs : accelerated ,
2023-02-12 21:43:12 -06:00
matchRate : matchRate ,
2023-06-09 13:46:14 -04:00
expectedFees : totalFees ,
expectedWeight : totalWeight ,
2023-02-12 21:43:12 -06:00
} ) ;
if ( block . extras ) {
block . extras . matchRate = matchRate ;
2023-06-06 08:52:29 +02:00
block . extras . expectedFees = totalFees ;
2023-06-10 12:09:06 -04:00
block . extras . expectedWeight = totalWeight ;
2023-03-14 10:19:32 +09:00
block . extras . similarity = similarity ;
2023-02-12 21:43:12 -06:00
}
2022-07-06 22:27:45 +02:00
}
2023-03-14 14:52:34 +09:00
} else if ( block . extras ) {
const mBlocks = mempoolBlocks . getMempoolBlocksWithTransactions ( ) ;
2023-03-16 22:15:20 +09:00
if ( mBlocks ? . length && mBlocks [ 0 ] . transactions ) {
block . extras . similarity = Common . getSimilarity ( mBlocks [ 0 ] , transactions ) ;
}
2020-06-08 02:08:51 +07:00
}
2023-12-31 18:05:25 +00:00
const confirmedTxids : { [ txid : string ] : boolean } = { } ;
2022-10-28 15:16:03 -06:00
// Update mempool to remove transactions included in the new block
for ( const txId of txIds ) {
delete _memPool [ txId ] ;
2022-12-14 16:51:53 -06:00
rbfCache . mined ( txId ) ;
2023-12-31 18:05:25 +00:00
confirmedTxids [ txId ] = true ;
2022-02-04 12:51:45 +09:00
}
2020-06-08 02:08:51 +07:00
2023-02-17 17:54:29 -06:00
if ( memPool . limitGBT ) {
const minFeeMempool = memPool . limitGBT ? await bitcoinSecondClient . getRawMemPool ( ) : null ;
const minFeeTip = memPool . limitGBT ? await bitcoinSecondClient . getBlockCount ( ) : - 1 ;
2024-01-07 17:57:36 +00:00
candidates = await memPool . getNextCandidates ( minFeeMempool , minFeeTip , transactions ) ;
2023-02-17 17:54:29 -06:00
transactionIds = Object . keys ( candidates ? . txs || { } ) ;
} else {
candidates = undefined ;
transactionIds = Object . keys ( memPool . getMempool ( ) ) ;
}
2024-01-06 18:26:03 +00:00
if ( config . MEMPOOL . RUST_GBT ) {
const added = memPool . limitGBT ? ( candidates ? . added || [ ] ) : [ ] ;
const removed = memPool . limitGBT ? ( candidates ? . removed || [ ] ) : transactions ;
await mempoolBlocks . $rustUpdateBlockTemplates ( transactionIds , _memPool , added , removed , candidates , true ) ;
2022-11-20 16:12:39 +09:00
} else {
2024-01-06 18:26:03 +00:00
await mempoolBlocks . $makeBlockTemplates ( transactionIds , _memPool , candidates , true , config . MEMPOOL_SERVICES . ACCELERATIONS ) ;
2022-11-16 18:18:59 -06:00
}
2022-11-20 16:12:39 +09:00
const mBlocks = mempoolBlocks . getMempoolBlocks ( ) ;
const mBlockDeltas = mempoolBlocks . getMempoolBlockDeltas ( ) ;
2022-10-28 15:16:03 -06:00
2022-06-01 00:03:25 +04:00
const da = difficultyAdjustment . getDifficultyAdjustment ( ) ;
const fees = feeApi . getRecommendedFee ( ) ;
2023-06-12 15:31:47 -04:00
const mempoolInfo = memPool . getMempoolInfo ( ) ;
2022-06-01 00:03:25 +04:00
2023-08-04 10:52:15 +09:00
// pre-compute address transactions
const addressCache = this . makeAddressCache ( transactions ) ;
2023-05-09 19:48:02 -06:00
// update init data
2023-06-12 15:31:47 -04:00
this . updateSocketDataFields ( {
'mempoolInfo' : mempoolInfo ,
'blocks' : [ . . . blocks . getBlocks ( ) , block ] . slice ( - config . MEMPOOL . INITIAL_BLOCKS_AMOUNT ) ,
'mempool-blocks' : mBlocks ,
'loadingIndicators' : loadingIndicators . getLoadingIndicators ( ) ,
'da' : da ? . previousTime ? da : undefined ,
'fees' : fees ,
} ) ;
2023-05-09 19:48:02 -06:00
2023-12-31 18:05:25 +00:00
const mBlocksWithTransactions = mempoolBlocks . getMempoolBlocksWithTransactions ( ) ;
2023-06-12 15:31:47 -04:00
const responseCache = { . . . this . socketData } ;
2023-05-09 19:48:02 -06:00
function getCachedResponse ( key , data ) : string {
2023-05-09 17:44:38 -06:00
if ( ! responseCache [ key ] ) {
responseCache [ key ] = JSON . stringify ( data ) ;
}
return responseCache [ key ] ;
}
2020-02-26 17:49:53 +07:00
this . wss . clients . forEach ( ( client ) = > {
if ( client . readyState !== WebSocket . OPEN ) {
return ;
}
2023-06-12 15:31:47 -04:00
const response = { } ;
if ( client [ 'want-blocks' ] ) {
response [ 'block' ] = getCachedResponse ( 'block' , block ) ;
2020-02-26 17:49:53 +07:00
}
2023-06-12 15:31:47 -04:00
if ( client [ 'want-stats' ] ) {
response [ 'mempoolInfo' ] = getCachedResponse ( 'mempoolInfo' , mempoolInfo ) ;
response [ 'vBytesPerSecond' ] = getCachedResponse ( 'vBytesPerSecond' , memPool . getVBytesPerSecond ( ) ) ;
response [ 'fees' ] = getCachedResponse ( 'fees' , fees ) ;
if ( da ? . previousTime ) {
response [ 'da' ] = getCachedResponse ( 'da' , da ) ;
}
}
2020-02-26 17:49:53 +07:00
2022-11-20 16:12:39 +09:00
if ( mBlocks && client [ 'want-mempool-blocks' ] ) {
2023-05-09 17:44:38 -06:00
response [ 'mempool-blocks' ] = getCachedResponse ( 'mempool-blocks' , mBlocks ) ;
2020-06-08 02:08:51 +07:00
}
2024-03-04 18:29:50 +00:00
if ( client [ 'want-tomahawk' ] ) {
2024-03-03 20:29:54 +00:00
response [ 'tomahawk' ] = getCachedResponse ( 'tomahawk' , bitcoinApi . getHealthStatus ( ) ) ;
}
2023-04-21 08:40:21 +09:00
if ( client [ 'track-tx' ] ) {
const trackTxid = client [ 'track-tx' ] ;
2023-12-31 18:05:25 +00:00
if ( trackTxid && confirmedTxids [ trackTxid ] ) {
2023-05-30 16:36:49 -04:00
response [ 'txConfirmed' ] = JSON . stringify ( trackTxid ) ;
2023-04-21 08:40:21 +09:00
} else {
const mempoolTx = _memPool [ trackTxid ] ;
if ( mempoolTx && mempoolTx . position ) {
2023-05-09 17:44:38 -06:00
response [ 'txPosition' ] = JSON . stringify ( {
2023-04-21 08:40:21 +09:00
txid : trackTxid ,
2023-06-13 13:35:25 -04:00
position : {
. . . mempoolTx . position ,
accelerated : mempoolTx.acceleration || undefined ,
}
2023-05-09 17:44:38 -06:00
} ) ;
2023-04-21 08:40:21 +09:00
}
}
2020-02-26 17:49:53 +07:00
}
2024-03-31 07:55:43 +00:00
if ( client [ 'track-txs' ] ) {
const txs : { [ txid : string ] : TxTrackingInfo } = { } ;
for ( const txid of client [ 'track-txs' ] ) {
if ( confirmedTxids [ txid ] ) {
txs [ txid ] = { confirmed : true } ;
} else {
const mempoolTx = _memPool [ txid ] ;
if ( mempoolTx && mempoolTx . position ) {
txs [ txid ] = {
position : {
. . . mempoolTx . position ,
} ,
accelerated : mempoolTx.acceleration || undefined ,
} ;
}
}
}
if ( Object . keys ( txs ) . length ) {
response [ 'tracked-txs' ] = JSON . stringify ( txs ) ;
}
}
2020-02-26 17:49:53 +07:00
if ( client [ 'track-address' ] ) {
2023-08-04 10:52:15 +09:00
const foundTransactions : TransactionExtended [ ] = Array . from ( addressCache [ client [ 'track-address' ] ] ? . values ( ) || [ ] ) ;
2023-07-26 10:47:59 +09:00
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 ) ;
}
}
2023-08-07 10:52:40 +09:00
if ( client [ 'track-addresses' ] ) {
const addressMap : { [ address : string ] : AddressTransactions } = { } ;
for ( const [ address , key ] of Object . entries ( client [ 'track-addresses' ] || { } ) ) {
const fullTransactions = Array . from ( addressCache [ key as string ] ? . values ( ) || [ ] ) ;
if ( fullTransactions ? . length ) {
addressMap [ address ] = {
mempool : [ ] ,
confirmed : fullTransactions ,
removed : [ ] ,
} ;
}
}
if ( Object . keys ( addressMap ) . length > 0 ) {
response [ 'multi-address-transactions' ] = JSON . stringify ( addressMap ) ;
}
}
2023-08-07 14:13:41 +09:00
if ( client [ 'track-scriptpubkeys' ] ) {
const spkMap : { [ spk : string ] : AddressTransactions } = { } ;
for ( const spk of client [ 'track-scriptpubkeys' ] || [ ] ) {
const fullTransactions = Array . from ( addressCache [ spk as string ] ? . values ( ) || [ ] ) ;
if ( fullTransactions ? . length ) {
spkMap [ spk ] = {
mempool : [ ] ,
confirmed : fullTransactions ,
removed : [ ] ,
} ;
}
}
if ( Object . keys ( spkMap ) . length > 0 ) {
response [ 'multi-scriptpubkey-transactions' ] = JSON . stringify ( spkMap ) ;
}
}
2020-05-05 15:26:23 +07:00
if ( client [ 'track-asset' ] ) {
const foundTransactions : TransactionExtended [ ] = [ ] ;
transactions . forEach ( ( tx ) = > {
2021-09-18 13:37:25 +04:00
if ( client [ 'track-asset' ] === Common . nativeAssetId ) {
2020-05-10 01:34:28 +07:00
if ( tx . vin && tx . vin . some ( ( vin ) = > ! ! vin . is_pegin ) ) {
foundTransactions . push ( tx ) ;
return ;
}
if ( tx . vout && tx . vout . some ( ( vout ) = > ! ! vout . pegout ) ) {
foundTransactions . push ( tx ) ;
}
} else {
if ( tx . vin && tx . vin . some ( ( vin ) = > ! ! vin . issuance && vin . issuance . asset_id === client [ 'track-asset' ] ) ) {
foundTransactions . push ( tx ) ;
return ;
}
if ( tx . vout && tx . vout . some ( ( vout ) = > ! ! vout . asset && vout . asset === client [ 'track-asset' ] ) ) {
foundTransactions . push ( tx ) ;
}
2020-02-26 17:49:53 +07:00
}
} ) ;
if ( foundTransactions . length ) {
2020-02-26 23:21:16 +07:00
foundTransactions . forEach ( ( tx ) = > {
tx . status = {
confirmed : true ,
block_height : block.height ,
block_hash : block.id ,
block_time : block.timestamp ,
} ;
} ) ;
2023-05-09 17:44:38 -06:00
response [ 'block-transactions' ] = JSON . stringify ( foundTransactions ) ;
2020-02-26 17:49:53 +07:00
}
}
2023-07-24 17:22:38 +09:00
if ( client [ 'track-mempool-block' ] >= 0 && memPool . isInSync ( ) ) {
2022-05-31 21:36:42 +00:00
const index = client [ 'track-mempool-block' ] ;
2023-12-31 18:05:25 +00:00
if ( mBlockDeltas && mBlockDeltas [ index ] && mBlocksWithTransactions [ index ] ? . transactions ? . length ) {
if ( mBlockDeltas [ index ] . added . length > ( mBlocksWithTransactions [ index ] ? . transactions . length / 2 ) ) {
response [ 'projected-block-transactions' ] = getCachedResponse ( ` projected-block-transactions-full- ${ index } ` , {
index : index ,
2024-02-08 22:40:22 +00:00
blockTransactions : mBlocksWithTransactions [ index ] . transactions . map ( mempoolBlocks . compressTx ) ,
2023-12-31 18:05:25 +00:00
} ) ;
} else {
response [ 'projected-block-transactions' ] = getCachedResponse ( ` projected-block-transactions-delta- ${ index } ` , {
index : index ,
delta : mBlockDeltas [ index ] ,
} ) ;
}
2022-05-31 21:36:42 +00:00
}
}
2023-06-12 15:31:47 -04:00
if ( Object . keys ( response ) . length ) {
2023-12-31 18:05:25 +00:00
client . send ( this . serializeResponse ( response ) ) ;
2023-06-12 15:31:47 -04:00
}
} ) ;
2024-02-10 20:45:58 +08:00
await statistics . runStatistics ( ) ;
2023-06-12 15:31:47 -04:00
}
// takes a dictionary of JSON serialized values
// and zips it together into a valid JSON object
private serializeResponse ( response ) : string {
return '{'
2023-05-09 17:44:38 -06:00
+ Object . keys ( response ) . map ( key = > ` " ${ key } ": ${ response [ key ] } ` ) . join ( ', ' )
+ '}' ;
2020-02-26 17:49:53 +07:00
}
2023-05-01 13:08:29 -06:00
2023-08-10 18:42:10 +09:00
// checks if an address conforms to a valid format
// returns the canonical form:
// - lowercase for bech32(m)
// - lowercase scriptpubkey for P2PK
// or false if invalid
private testAddress ( address ) : string | false {
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}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/ . test ( address ) ) {
if ( /^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/ . test ( address ) ) {
address = address . toLowerCase ( ) ;
}
if ( /^04[a-fA-F0-9]{128}$/ . test ( address ) ) {
return '41' + address + 'ac' ;
} else if ( /^(02|03)[a-fA-F0-9]{64}$/ . test ( address ) ) {
return '21' + address + 'ac' ;
} else {
return address ;
}
} else {
return false ;
}
}
2023-08-04 10:52:15 +09:00
private makeAddressCache ( transactions : MempoolTransactionExtended [ ] ) : { [ address : string ] : Set < MempoolTransactionExtended > } {
const addressCache : { [ address : string ] : Set < MempoolTransactionExtended > } = { } ;
for ( const tx of transactions ) {
for ( const vin of tx . vin ) {
if ( vin ? . prevout ? . scriptpubkey_address ) {
if ( ! addressCache [ vin . prevout . scriptpubkey_address ] ) {
addressCache [ vin . prevout . scriptpubkey_address ] = new Set ( ) ;
}
addressCache [ vin . prevout . scriptpubkey_address ] . add ( tx ) ;
}
if ( vin ? . prevout ? . scriptpubkey ) {
if ( ! addressCache [ vin . prevout . scriptpubkey ] ) {
addressCache [ vin . prevout . scriptpubkey ] = new Set ( ) ;
}
addressCache [ vin . prevout . scriptpubkey ] . add ( tx ) ;
}
}
for ( const vout of tx . vout ) {
if ( vout ? . scriptpubkey_address ) {
if ( ! addressCache [ vout ? . scriptpubkey_address ] ) {
addressCache [ vout ? . scriptpubkey_address ] = new Set ( ) ;
}
addressCache [ vout ? . scriptpubkey_address ] . add ( tx ) ;
}
if ( vout ? . scriptpubkey ) {
if ( ! addressCache [ vout . scriptpubkey ] ) {
addressCache [ vout . scriptpubkey ] = new Set ( ) ;
}
addressCache [ vout . scriptpubkey ] . add ( tx ) ;
}
}
}
return addressCache ;
}
private async getFullTransactions ( transactions : MempoolTransactionExtended [ ] ) : Promise < MempoolTransactionExtended [ ] > {
for ( let i = 0 ; i < transactions . length ; i ++ ) {
try {
transactions [ i ] = await transactionUtils . $getMempoolTransactionExtended ( transactions [ i ] . txid , true ) ;
} catch ( e ) {
logger . debug ( 'Error finding transaction in mempool: ' + ( e instanceof Error ? e.message : e ) ) ;
}
}
return transactions ;
}
2023-05-01 13:08:29 -06:00
private printLogs ( ) : void {
if ( this . wss ) {
2023-12-31 18:05:25 +00:00
let numTxSubs = 0 ;
2024-03-31 07:55:43 +00:00
let numTxsSubs = 0 ;
2023-12-31 18:05:25 +00:00
let numProjectedSubs = 0 ;
let numRbfSubs = 0 ;
this . wss . clients . forEach ( ( client ) = > {
if ( client [ 'track-tx' ] ) {
numTxSubs ++ ;
}
2024-03-31 07:55:43 +00:00
if ( client [ 'track-txs' ] ) {
numTxsSubs ++ ;
}
2024-01-02 16:26:57 +00:00
if ( client [ 'track-mempool-block' ] != null && client [ 'track-mempool-block' ] >= 0 ) {
2023-12-31 18:05:25 +00:00
numProjectedSubs ++ ;
}
if ( client [ 'track-rbf' ] ) {
numRbfSubs ++ ;
}
} )
2023-05-01 13:08:29 -06:00
const count = this . wss ? . clients ? . size || 0 ;
const diff = count - this . numClients ;
this . numClients = count ;
logger . debug ( ` ${ count } websocket clients | ${ this . numConnected } connected | ${ this . numDisconnected } disconnected | ( ${ diff >= 0 ? '+' : '' } ${ diff } ) ` ) ;
2024-03-31 07:55:43 +00:00
logger . debug ( ` websocket subscriptions: track-tx: ${ numTxSubs } , track-txs: ${ numTxsSubs } , track-mempool-block: ${ numProjectedSubs } track-rbf: ${ numRbfSubs } ` ) ;
2023-05-01 13:08:29 -06:00
this . numConnected = 0 ;
this . numDisconnected = 0 ;
}
}
2020-02-26 17:49:53 +07:00
}
export default new WebsocketHandler ( ) ;