2022-08-02 12:19:57 +02:00
import { existsSync , promises } from 'fs' ;
2022-08-01 17:25:44 +02:00
import bitcoinClient from '../../../api/bitcoin/bitcoin-client' ;
2022-08-09 09:21:31 +02:00
import { Common } from '../../../api/common' ;
2022-08-01 17:25:44 +02:00
import config from '../../../config' ;
import logger from '../../../logger' ;
2022-08-02 12:19:57 +02:00
const fsPromises = promises ;
2022-08-01 17:25:44 +02:00
const BLOCKS_CACHE_MAX_SIZE = 100 ;
const CACHE_FILE_NAME = config . MEMPOOL . CACHE_DIR + '/ln-funding-txs-cache.json' ;
class FundingTxFetcher {
private running = false ;
private blocksCache = { } ;
private channelNewlyProcessed = 0 ;
public fundingTxCache = { } ;
2022-08-02 16:39:34 +02:00
async $init ( ) : Promise < void > {
2022-08-01 17:25:44 +02:00
// Load funding tx disk cache
if ( Object . keys ( this . fundingTxCache ) . length === 0 && existsSync ( CACHE_FILE_NAME ) ) {
try {
2022-08-02 12:19:57 +02:00
this . fundingTxCache = JSON . parse ( await fsPromises . readFile ( CACHE_FILE_NAME , 'utf-8' ) ) ;
2022-08-01 17:25:44 +02:00
} catch ( e ) {
2022-12-01 15:52:06 +01:00
logger . err ( ` Unable to parse channels funding txs disk cache. Starting from scratch ` , logger . tags . ln ) ;
2022-08-01 17:25:44 +02:00
this . fundingTxCache = { } ;
}
2022-12-01 15:52:06 +01:00
logger . debug ( ` Imported ${ Object . keys ( this . fundingTxCache ) . length } funding tx amount from the disk cache ` , logger . tags . ln ) ;
2022-08-01 17:25:44 +02:00
}
2022-08-02 16:39:34 +02:00
}
async $fetchChannelsFundingTxs ( channelIds : string [ ] ) : Promise < void > {
if ( this . running ) {
return ;
}
this . running = true ;
2022-08-01 17:25:44 +02:00
const globalTimer = new Date ( ) . getTime ( ) / 1000 ;
let cacheTimer = new Date ( ) . getTime ( ) / 1000 ;
let loggerTimer = new Date ( ) . getTime ( ) / 1000 ;
let channelProcessed = 0 ;
this . channelNewlyProcessed = 0 ;
for ( const channelId of channelIds ) {
await this . $fetchChannelOpenTx ( channelId ) ;
++ channelProcessed ;
let elapsedSeconds = Math . round ( ( new Date ( ) . getTime ( ) / 1000 ) - loggerTimer ) ;
2022-12-01 15:52:06 +01:00
if ( elapsedSeconds > config . LIGHTNING . LOGGER_UPDATE_INTERVAL ) {
2022-08-01 17:25:44 +02:00
elapsedSeconds = Math . round ( ( new Date ( ) . getTime ( ) / 1000 ) - globalTimer ) ;
2022-08-03 12:13:55 +02:00
logger . info ( ` Indexing channels funding tx ${ channelProcessed + 1 } of ${ channelIds . length } ` +
2022-08-01 17:25:44 +02:00
` ( ${ Math . floor ( channelProcessed / channelIds . length * 10000 ) / 100 } %) | ` +
2022-12-01 15:52:06 +01:00
` elapsed: ${ elapsedSeconds } seconds ` ,
logger . tags . ln
2022-08-01 17:25:44 +02:00
) ;
loggerTimer = new Date ( ) . getTime ( ) / 1000 ;
}
elapsedSeconds = Math . round ( ( new Date ( ) . getTime ( ) / 1000 ) - cacheTimer ) ;
if ( elapsedSeconds > 60 ) {
2022-12-01 15:52:06 +01:00
logger . debug ( ` Saving ${ Object . keys ( this . fundingTxCache ) . length } funding txs cache into disk ` , logger . tags . ln ) ;
2022-08-02 12:19:57 +02:00
fsPromises . writeFile ( CACHE_FILE_NAME , JSON . stringify ( this . fundingTxCache ) ) ;
2022-08-01 17:25:44 +02:00
cacheTimer = new Date ( ) . getTime ( ) / 1000 ;
}
}
if ( this . channelNewlyProcessed > 0 ) {
2022-12-01 15:52:06 +01:00
logger . info ( ` Indexed ${ this . channelNewlyProcessed } additional channels funding tx ` , logger . tags . ln ) ;
logger . debug ( ` Saving ${ Object . keys ( this . fundingTxCache ) . length } funding txs cache into disk ` , logger . tags . ln ) ;
2022-08-02 12:19:57 +02:00
fsPromises . writeFile ( CACHE_FILE_NAME , JSON . stringify ( this . fundingTxCache ) ) ;
2022-08-01 17:25:44 +02:00
}
this . running = false ;
}
2022-12-06 10:51:01 +01:00
public async $fetchChannelOpenTx ( channelId : string ) : Promise < { timestamp : number , txid : string , value : number } | null > {
2022-08-18 10:59:03 +02:00
channelId = Common . channelIntegerIdToShortId ( channelId ) ;
2022-08-09 09:21:31 +02:00
2022-08-01 17:25:44 +02:00
if ( this . fundingTxCache [ channelId ] ) {
return this . fundingTxCache [ channelId ] ;
}
const parts = channelId . split ( 'x' ) ;
const blockHeight = parts [ 0 ] ;
const txIdx = parts [ 1 ] ;
const outputIdx = parts [ 2 ] ;
let block = this . blocksCache [ blockHeight ] ;
2022-08-02 12:19:57 +02:00
// Fetch it from core
2022-08-01 17:25:44 +02:00
if ( ! block ) {
const blockHash = await bitcoinClient . getBlockHash ( parseInt ( blockHeight , 10 ) ) ;
2022-08-02 13:03:32 +02:00
block = await bitcoinClient . getBlock ( blockHash , 1 ) ;
2022-08-01 17:25:44 +02:00
}
2022-08-02 12:19:57 +02:00
this . blocksCache [ block . height ] = block ;
2022-08-01 17:25:44 +02:00
2022-08-02 12:19:57 +02:00
const blocksCacheHashes = Object . keys ( this . blocksCache ) . sort ( ( a , b ) = > parseInt ( b ) - parseInt ( a ) ) . reverse ( ) ;
2022-08-01 17:25:44 +02:00
if ( blocksCacheHashes . length > BLOCKS_CACHE_MAX_SIZE ) {
for ( let i = 0 ; i < 10 ; ++ i ) {
delete this . blocksCache [ blocksCacheHashes [ i ] ] ;
}
}
2022-08-02 13:03:32 +02:00
const txid = block . tx [ txIdx ] ;
const rawTx = await bitcoinClient . getRawTransaction ( txid ) ;
const tx = await bitcoinClient . decodeRawTransaction ( rawTx ) ;
2022-12-06 10:51:01 +01:00
if ( ! tx || ! tx . vout || tx . vout . length < parseInt ( outputIdx , 10 ) + 1 || tx . vout [ outputIdx ] . value === undefined ) {
logger . err ( ` Cannot find blockchain funding tx for channel id ${ channelId } . Possible reasons are: bitcoin backend timeout or the channel shortId is not valid ` ) ;
return null ;
}
2022-08-01 17:25:44 +02:00
this . fundingTxCache [ channelId ] = {
timestamp : block.time ,
2022-08-02 13:03:32 +02:00
txid : txid ,
value : tx.vout [ outputIdx ] . value ,
2022-08-01 17:25:44 +02:00
} ;
++ this . channelNewlyProcessed ;
return this . fundingTxCache [ channelId ] ;
}
}
2022-08-08 09:00:11 +02:00
export default new FundingTxFetcher ;