import { UtxoInfo , BalanceActivity , BalanceActivityCallback , BalanceMonitor , GetBalanceActivityOptions , RetrieveBalanceActivitiesResult , NetworkType , createUnitConverters , NewBlockCallback , FilterBlockAddressesCallback , BlockInfo , } from '@bitaccess/coinlib-common' ; import { EventEmitter } from 'events' ; import { AddressDetailsBitcoinTxs , NormalizedTxBitcoin , NormalizedTxBitcoinVin , NormalizedTxBitcoinVout , } from 'blockbook-client' ; import BigNumber from 'bignumber.js' ; import { isUndefined , Numeric } from '@faast/ts-common' ; import { BitcoinishBalanceMonitorConfig , BitcoinishBlock } from './types' ; import { BlockbookConnected } from './BlockbookConnected' ; import { BitcoinishPaymentsUtils } from './BitcoinishPaymentsUtils' ; import { omit } from 'lodash' ; export abstract class BitcoinishBalanceMonitor extends BlockbookConnected implements BalanceMonitor { readonly coinName : string ; readonly coinSymbol : string ; readonly utils : BitcoinishPaymentsUtils ; readonly events = new EventEmitter ( ) ; constructor ( config ) { super ( config ) ; this . utils = config . utils ; this . coinName = config . utils . coinName ; this . coinSymbol = config . utils . coinSymbol ; } async destroy ( ) { this . events . removeAllListeners ( 'tx' ) ; await super . destroy ( ) ; } async subscribeAddresses ( addresses ) { for ( const address of addresses ) { this . utils . validateAddress ( address ) ; } await this . getApi ( ) . subscribeAddresses ( addresses , async ( { address , tx } ) => { this . events . emit ( 'tx' , { address , tx } ) ; const activity = await this . txToBalanceActivity ( address , tx as NormalizedTxBitcoin ) ; if ( activity ) { this . events . emit ( 'activity' , { activity , tx } ) ; } } ) ; } async subscribeNewBlock ( callbackFn ) { await this . getApi ( ) . subscribeNewBlock ( callbackFn ) ; } onBalanceActivity ( callbackFn ) { this . events . on ( 'activity' , ( { activity , tx } ) => { callbackFn ( activity , tx ) ?. catch ( ( e ) => this . logger . error ( `Error in ${ this . coinSymbol } ${ this . networkType } onBalanceActivity callback` , e ) ) ; } ) ; } private accumulateAddressTx ( addressTransactions , tx , inout ) { if ( ! ( inout . isAddress && inout . addresses ?. length ) ) { return ; } const address = this . utils . standardizeAddress ( inout . addresses [ 0 ] ) ; if ( address === null ) { return ; } ( addressTransactions [ address ] = addressTransactions [ address ] ?? new Set ( ) ) . add ( tx ) ; } async retrieveBlockBalanceActivities ( blockId , callbackFn , filterRelevantAddresses ) { let page = 1 ; let blockRaw ; let blockInfoResult ; while ( ! blockRaw || blockRaw . page < blockRaw . totalPages ) { const getBlockResult = await this . utils . getBlock ( blockId , { page , includeTxs : true } ) ; blockInfoResult = omit ( getBlockResult , [ 'raw.txs' ] ) as BlockInfo ; blockRaw = getBlockResult . raw as BitcoinishBlock ; if ( ! blockRaw ?. txs ) { this . logger . log ( `No transactions returned for page ${ page } of block ${ blockId }` ) ; break ; } const addressTransactions = { } ; for ( const tx of blockRaw . txs ) { for ( const input of tx . vin ) { this . accumulateAddressTx ( addressTransactions , tx , input ) ; } for ( const output of tx . vout ) { this . accumulateAddressTx ( addressTransactions , tx , output ) ; } } const relevantAddresses = new Set < string > ( await filterRelevantAddresses ( Array . from ( Object . keys ( addressTransactions ) ) , { ... blockInfoResult , page } ) ) ; /**
             * Block tx data doesn't always include input txids which is needed for determining utxosSpent.
             * Hard lookup is needed for each tx, memoize to avoid redundant lookups.
             */ const hardTxQueries = { } ; for ( const address of relevantAddresses ) { const txs = addressTransactions [ address ] ?? [ ] ; for ( const { txid } of txs ) { const tx = ( hardTxQueries [ txid ] ??= await this . _retryDced ( ( ) => this . getApi ( ) . getTx ( txid ) ) ) ; const activity = await this . txToBalanceActivity ( address , tx ) ; if ( activity ) { await callbackFn ( activity , tx ) ; } } } page ++ ; } return blockInfoResult ! ; } async retrieveBalanceActivities ( address , callbackFn , options = { } ) { this . utils . validateAddress ( address ) ; const { from : fromOption , to : toOption } = options ; const from = new BigNumber ( isUndefined ( fromOption ) ? 0 : ( Numeric . is ( fromOption ) ? fromOption : fromOption . confirmationNumber ) ) . toNumber ( ) ; const to = new BigNumber ( isUndefined ( toOption ) ? 'Infinity' : ( Numeric . is ( toOption ) ? toOption . toString ( ) : toOption . confirmationNumber ) ) . toNumber ( ) ; let page = 1 ; const limit = 10 ; let lastTx ; let transactionPage ; let transactions ; while ( isUndefined ( transactionPage ) || transactionPage . page < transactionPage . totalPages || transactionPage . totalPages === - 1 ) { transactionPage = await this . _retryDced ( ( ) => this . getApi ( ) . getAddressDetails ( address , { details : 'txs' , page , pageSize : limit , from , to : to < Infinity ? to : undefined , } ) ) ; if ( transactionPage . page !== page ) { break ; } transactions = transactionPage . transactions ; this . logger . debug ( `retrieved txs for ${ address }` , transactions ) ; if ( ! transactions || transactions . length === 0 ) { break ; } for ( const tx of transactions ) { if ( lastTx && tx . txid === lastTx . txid ) { this . logger . debug ( 'ignoring duplicate tx' , tx ) ; continue ; } if ( tx . blockHeight > 0 && ( from > tx . blockHeight || to < tx . blockHeight ) ) { this . logger . debug ( 'ignoring out of range balance activity tx' , tx ) ; continue ; } const activity = await this . txToBalanceActivity ( address , tx ) ; if ( activity ) { await callbackFn ( activity , tx ) ; } } lastTx = transactions [ transactions . length - 1 ] ; page ++ ; } return { from : from . toString ( ) , to : to . toString ( ) } ; } private extractStandardAddress ( v ) { const address = v . isAddress && v . addresses ?. [ 0 ] ; return address ? this . utils . standardizeAddress ( address ) : null ; } async txToBalanceActivity ( address , tx ) { const externalId = tx . txid ; const confirmationNumber = tx . blockHeight ; const standardizedAddress = this . utils . standardizeAddress ( address ) ; if ( standardizedAddress === null ) { this . logger . warn ( `Cannot standardize ${ this . coinName } address, likely invalid: ${ address }` ) ; return null ; } let netSatoshis = new BigNumber ( 0 ) ; const utxosSpent = [ ] ; const utxosCreated = [ ] ; for ( const input of tx . vin ) { if ( this . extractStandardAddress ( input ) === standardizedAddress ) { netSatoshis = netSatoshis . minus ( input . value ) ; const inputTxid = input . txid ; if ( ! inputTxid ) { this . logger . log ( `Tx ${ tx . txid } input ${ input . n } has no txid or vout` , input ) ; continue ; } const vout = input . vout ?? 0 ; const inputTxInfo = await this . _retryDced ( ( ) => this . getApi ( ) . getTx ( inputTxid ) ) ; const output = inputTxInfo . vout [ vout ] ; utxosSpent . push ( { txid : inputTxid , vout , satoshis : new BigNumber ( input . value ) . toNumber ( ) , value : this . utils . toMainDenominationString ( input . value ) , confirmations : inputTxInfo . confirmations , height : inputTxInfo . blockHeight > 0 ? String ( inputTxInfo . blockHeight ) : undefined , coinbase : ! input . isAddress && input . value === '0' , lockTime : inputTxInfo . lockTime ? String ( inputTxInfo . lockTime ) : undefined , txHex : inputTxInfo . hex , scriptPubKeyHex : output . hex , address : standardizedAddress , spent : true , } ) ; } } for ( const output of tx . vout ) { if ( this . extractStandardAddress ( output ) === standardizedAddress ) { netSatoshis = netSatoshis . plus ( output . value ) ; utxosCreated . push ( this . utils . txVoutToUtxoInfo ( tx , output ) ) ; } } if ( ! ( utxosSpent . length || utxosCreated . length ) ) { this . logger . log ( `${ this . coinName } transaction ${ externalId } does not affect balance of ${ standardizedAddress }` , tx ) ; return null ; } return { type : netSatoshis . gt ( 0 ) ? 'in' : 'out' , networkType : this . networkType , networkSymbol : this . coinSymbol , assetSymbol : this . coinSymbol , address : address , extraId : null , amount : this . utils . toMainDenominationString ( netSatoshis ) , externalId : tx . txid , activitySequence : '' , confirmationId : tx . blockHash ?? '' , confirmationNumber : confirmationNumber > 0 ? confirmationNumber : - 1 , confirmations : tx . confirmations , timestamp : new Date ( tx . blockTime * 1000 ) , utxosSpent , utxosCreated , } ; } }