import { Logger } from 'fastify' ; import moment from 'moment' ; import { Pool , QueryResult } from 'pg' ; import { Block , FindTransaction , FindTransactionWithToken , GenesisBlock , PolicyId , PopulatedTransaction , Token , Transaction , TransactionCount , TransactionInOut , TransactionPoolRegistrations , Utxo , MaBalance , TotalCount , SearchFilters } from '../models' ; import { LinearFeeParameters } from '../services/cardano-services' ; import { hexStringToBuffer , hexFormatter , isEmptyHexString , remove0xPrefix , coinIdentifierFormatter } from '../utils/formatters' ; import Queries , { FindBalance , FindTransactionDelegations , FindTransactionDeregistrations , FindTransactionFieldResult , FindTransactionInOutResult , FindTransactionRegistrations , FindTransactionsInputs , FindTransactionsOutputs , FindTransactionPoolRegistrationsData , FindTransactionPoolRelays , FindTransactionPoolOwners , FindTransactionWithdrawals , FindTransactionMetadata , FindPoolRetirements , FindUtxo , FindMaBalance } from './queries/blockchain-queries' ; import SearchQueries from './queries/search-transactions-queries' ; import { ADA , CatalystDataIndexes , CatalystSigIndexes , OperatorType } from '../utils/constants' ; import { isVoteDataValid , isVoteSignatureValid , validateAndGetStatus , validateTransactionCoinMatch , validateAccountIdAddressMatch , validateOperationType , validateCurrencies } from '../utils/validations' ; import { getAddressFromHexString } from '../utils/cardano/addresses' ; export interface BlockchainRepository { param blockNumber param blockHash findBlock ( logger , number ? , blockHash ? ) : Promise < Block | null > ; param hashes fillTransaction ( logger , transactions ) : Promise < PopulatedTransaction [ ] > ; param number param blockHash findTransactionsByBlock ( logger , number ? , blockHash ? ) : Promise < Transaction [ ] > ; param hash findTransactionByHashAndBlock ( logger , hash , blockNumber , blockHash ) : Promise < PopulatedTransaction | null > ; /**
     * Returns the tip of the chain block number
     */ findLatestBlockNumber ( logger ) : Promise < number > ; /**
     * Returns the genesis block
     */ findGenesisBlock ( logger ) : Promise < GenesisBlock | null > ; /**
     * Get the lastest minFeeA and minFeeB
     */ getLinearFeeParameters ( logger ) : Promise < LinearFeeParameters > ; param address param blockIdentifier findUtxoByAddressAndBlock ( logger , address , blockHash , currencies ? ) : Promise < Utxo [ ] > ; param address param blockIdentifier findMultiAssetByAddressAndBlock ( logger , address , blockHash ) : Promise < MaBalance [ ] > ; param address param blockIdentifier findBalanceByAddressAndBlock ( logger , address , blockHash ) : Promise < string > ; param filters findTransactionsByFilters ( logger , parameters , limit , offset ) : Promise < TransactionCount > ; } /**
 * Maps from a transaction query result to a Transaction Array
 */ const parseTransactionRows = ( result ) : Transaction [ ] => result . rows . map ( row => ( { hash : hexFormatter ( row . hash ) , blockHash : hexFormatter ( row . blockHash ) , blockNo : row . blockNo , fee : row . fee , size : row . size , scriptSize : row . scriptSize , validContract : row . validContract } ) ) ; /**
 * Maps from a total count query result to a number
 */ const parseTotalCount = ( result ) : number => result . rows [ 0 ] . totalCount ; param transactions const mapTransactionsToDict = ( transactions ) : TransactionsMap => transactions . reduce ( ( mappedTransactions , transaction ) => { const hash = transaction . hash ; return { ... mappedTransactions , [ hash ] : { hash , blockHash : transaction . blockHash , blockNo : transaction . blockNo , fee : transaction . fee , size : transaction . size , scriptSize : transaction . scriptSize , validContract : transaction . validContract , inputs : [ ] , outputs : [ ] , withdrawals : [ ] , registrations : [ ] , deregistrations : [ ] , delegations : [ ] , poolRegistrations : [ ] , poolRetirements : [ ] , voteRegistrations : [ ] } } ; } , { } ) ; const mapToTransactionPoolRegistrations = ( poolRegistrations , poolOwners , poolRelays ) : TransactionPoolRegistrations [ ] => poolRegistrations . map ( poolRegistration => { const owners = poolOwners . filter ( owner => owner . poolId === poolRegistration . poolId ) . map ( o => o . owner ) ; const relays = poolRelays . filter ( relay => relay . updateId === poolRegistration . updateId ) . map ( r => ( { ipv4 : r . ipv4 , ipv6 : r . ipv6 , dnsName : r . dnsName , port : r . port } ) ) ; return { txHash : poolRegistration . txHash , vrfKeyHash : poolRegistration . vrfKeyHash , pledge : poolRegistration . pledge , margin : poolRegistration . margin , cost : poolRegistration . cost , address : poolRegistration . address , poolHash : poolRegistration . poolHash , owners , relays , metadataUrl : poolRegistration . metadataUrl , metadataHash : poolRegistration . metadataHash } ; } ) ; type TransactionsMap = NodeJS . Dict < PopulatedTransaction > ; const populateTransactionField = < T extends FindTransactionFieldResult > ( transactionsMap , queryResults , populateTransaction ) : TransactionsMap => queryResults . reduce ( ( updatedTransactionsMap , row ) => { const transaction = updatedTransactionsMap [ hexFormatter ( row . txHash ) ] ; if ( transaction ) { const updatedTransaction = populateTransaction ( transaction , row ) ; return { ... updatedTransactionsMap , [ transaction . hash ] : updatedTransaction } ; } return updatedTransactionsMap ; } , transactionsMap ) ; param { FindTransactionInOutResult } operation returns const hasToken = ( operation ) : operation is FindTransactionWithToken => operation . policy !== null && operation . name !== null && operation . quantity !== null ; const tryAddToken = < T extends TransactionInOut > ( inOut , findResult ) : T => { if ( hasToken ( findResult ) ) { const { policy , name , quantity } = findResult ; const policyAsHex = hexFormatter ( policy ) ; const nameAsHex = hexFormatter ( name ) ; const tokenBundle = inOut . tokenBundle ?? { tokens : new Map < PolicyId , Token [ ] > ( ) } ; if ( ! tokenBundle . tokens . has ( policyAsHex ) ) { tokenBundle . tokens . set ( policyAsHex , [ ] ) ; } tokenBundle . tokens . get ( policyAsHex ) ?. push ( { name : nameAsHex , quantity } ) ; return { ... inOut , tokenBundle } ; } return inOut ; } ; param row param transaction param getCollection param createInstance param updateCollection const parseInOutRow = < T extends TransactionInOut , F extends FindTransactionInOutResult > ( row , transaction , getCollection , createInstance , updateCollection ) : PopulatedTransaction => { const collection = getCollection ( transaction ) ; const index = collection . findIndex ( i => i . id === row . id ) ; if ( index !== - 1 ) { const updated = tryAddToken ( collection [ index ] , row ) ; const newCollection = [ ... collection ] ; newCollection [ index ] = updated ; return updateCollection ( { ... transaction } , newCollection ) ; } const newInstance = createInstance ( row ) ; const newInOut = tryAddToken ( newInstance , row ) ; return updateCollection ( { ... transaction } , collection . concat ( newInOut ) ) ; } ; param populatedTransaction param input const parseInputsRow = ( populatedTransaction , input ) : PopulatedTransaction => parseInOutRow ( input , populatedTransaction , transaction => transaction . inputs , queryResult => ( { id : queryResult . id , address : queryResult . address , value : queryResult . value , sourceTransactionHash : hexFormatter ( queryResult . sourceTxHash ) , sourceTransactionIndex : queryResult . sourceTxIndex } ) , ( updatedTransaction , updatedCollection ) => ( { ... updatedTransaction , inputs : updatedCollection } ) ) ; param transaction param deregistration const parsePoolRetirementRow = ( transaction , poolRetirement ) : PopulatedTransaction => ( { ... transaction , poolRetirements : transaction . poolRetirements . concat ( { epoch : poolRetirement . epoch , address : hexFormatter ( poolRetirement . address ) } ) } ) ; param transaction param vote const parseVoteRow = ( transaction , metadata ) : PopulatedTransaction => { const { data , signature } = metadata ; if ( isVoteDataValid ( data ) && isVoteSignatureValid ( signature ) ) { const votingKey = remove0xPrefix ( data [ CatalystDataIndexes . VOTING_KEY ] ) ; const stakeKey = remove0xPrefix ( data [ CatalystDataIndexes . STAKE_KEY ] ) ; const rewardAddress = getAddressFromHexString ( remove0xPrefix ( data [ CatalystDataIndexes . REWARD_ADDRESS ] ) ) . to_bech32 ( ) ; const votingNonce = data [ CatalystDataIndexes . VOTING_NONCE ] ; const votingSignature = remove0xPrefix ( signature [ CatalystSigIndexes . VOTING_SIGNATURE ] ) ; return { ... transaction , voteRegistrations : transaction . voteRegistrations . concat ( { votingKey , stakeKey , rewardAddress , votingNonce , votingSignature } ) } ; } return transaction ; } ; param populatedTransaction param output const parseOutputsRow = ( populatedTransaction , output ) : PopulatedTransaction => parseInOutRow ( output , populatedTransaction , transaction => transaction . outputs , queryResult => ( { id : queryResult . id , address : queryResult . address , value : queryResult . value , index : queryResult . index } ) , ( updatedTransaction , updatedCollection ) => ( { ... updatedTransaction , outputs : updatedCollection } ) ) ; param transaction param withdrawal const parseWithdrawalsRow = ( transaction , withdrawal ) : PopulatedTransaction => ( { ... transaction , withdrawals : transaction . withdrawals . concat ( { stakeAddress : withdrawal . address , amount : withdrawal . amount } ) } ) ; param transaction param registration const parseRegistrationsRow = ( transaction , registration ) : PopulatedTransaction => ( { ... transaction , registrations : transaction . registrations . concat ( { stakeAddress : registration . address , amount : registration . amount } ) } ) ; param transaction param deregistration const parseDeregistrationsRow = ( transaction , deregistration ) : PopulatedTransaction => ( { ... transaction , deregistrations : transaction . deregistrations . concat ( { stakeAddress : deregistration . address , amount : deregistration . amount } ) } ) ; param transaction param delegation const parseDelegationsRow = ( transaction , delegation ) : PopulatedTransaction => ( { ... transaction , delegations : transaction . delegations . concat ( { stakeAddress : delegation . address , poolHash : hexFormatter ( delegation . poolHash ) } ) } ) ; const parsePoolRegistrationsRows = ( transaction , poolRegistration ) : PopulatedTransaction => ( { ... transaction , poolRegistrations : transaction . poolRegistrations . concat ( { vrfKeyHash : hexFormatter ( poolRegistration . vrfKeyHash ) , pledge : poolRegistration . pledge , margin : poolRegistration . margin , cost : poolRegistration . cost , address : hexFormatter ( poolRegistration . address ) , poolHash : hexFormatter ( poolRegistration . poolHash ) , owners : poolRegistration . owners . map ( o => hexFormatter ( o ) ) , relays : poolRegistration . relays , metadataUrl : poolRegistration . metadataUrl , metadataHash : poolRegistration . metadataHash ? hexFormatter ( poolRegistration . metadataHash ) : undefined } ) } ) ; const populateTransactions = async ( databaseInstance , transactionsMap ) : Promise < PopulatedTransaction [ ] > => { const transactionsHashes = Object . keys ( transactionsMap ) . map ( hexStringToBuffer ) ; const operationsQueries = [ Queries . findTransactionsInputs , Queries . findTransactionsOutputs , Queries . findTransactionWithdrawals , Queries . findTransactionRegistrations , Queries . findTransactionDeregistrations , Queries . findTransactionDelegations , Queries . findTransactionMetadata , Queries . findTransactionPoolRegistrationsData , Queries . findTransactionPoolOwners , Queries . findTransactionPoolRelays , Queries . findPoolRetirements ] ; const [ inputs , outputs , withdrawals , registrations , deregistrations , delegations , votes , poolsData , poolsOwners , poolsRelays , poolRetirements ] = await Promise . all ( operationsQueries . map ( operationQuery => databaseInstance . query ( operationQuery , [ transactionsHashes ] ) ) ) ; transactionsMap = populateTransactionField ( transactionsMap , inputs . rows , parseInputsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , outputs . rows , parseOutputsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , withdrawals . rows , parseWithdrawalsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , registrations . rows , parseRegistrationsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , deregistrations . rows , parseDeregistrationsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , delegations . rows , parseDelegationsRow ) ; transactionsMap = populateTransactionField ( transactionsMap , poolRetirements . rows , parsePoolRetirementRow ) ; transactionsMap = populateTransactionField ( transactionsMap , votes . rows , parseVoteRow ) ; const mappedPoolRegistrations = mapToTransactionPoolRegistrations ( poolsData . rows , poolsOwners . rows , poolsRelays . rows ) ; transactionsMap = populateTransactionField ( transactionsMap , mappedPoolRegistrations , parsePoolRegistrationsRows ) ; return Object . values ( transactionsMap ) ; } ; export const configure = ( databaseInstance ) : BlockchainRepository => ( { async findBlock ( logger , blockNumber ? , blockHash ? ) { const query = Queries . findBlock ( blockNumber , blockHash ) ; logger . debug ( `[findBlock] Parameters received for run query blockNumber: ${ blockNumber }, blockHash: ${ blockHash }` ) ; const parameters = [ blockNumber ? blockNumber : true , blockHash ? hexStringToBuffer ( blockHash ) : true ] ; const result = await databaseInstance . query ( query , parameters ) ; if ( result . rows . length === 1 ) { logger . debug ( '[findBlock] Block found!' ) ; const { number , hash , createdAt , previousBlockHash , previousBlockNumber , transactionsCount , createdBy , size , epochNo , slotNo } = result . rows [ 0 ] ; return { number , hash : hexFormatter ( hash ) , createdAt : moment . utc ( createdAt ) . unix ( ) * 1000 , previousBlockHash : previousBlockHash && hexFormatter ( previousBlockHash ) , previousBlockNumber , transactionsCount , createdBy , size , epochNo , slotNo } ; } logger . debug ( '[findBlock] No block was found' ) ; return null ; } , async findTransactionsByBlock ( logger , blockNumber ? , blockHash ? ) { const query = Queries . findTransactionsByBlock ( blockNumber , blockHash ) ; logger . debug ( `[findTransactionsByBlock] Parameters received for run query blockNumber: ${ blockNumber }, blockHash: ${ blockHash }` ) ; const parameters = [ blockNumber ? blockNumber : true , blockHash ? hexStringToBuffer ( blockHash ) : true ] ; logger . debug ( { parameters } , '[findTransactionsByBlock] About to run findTransactionsByBlock query with params' ) ; const result = await databaseInstance . query ( query , parameters ) ; logger . debug ( `[findTransactionsByBlock] Found ${ result . rowCount } transactions` ) ; if ( result . rows . length > 0 ) { return parseTransactionRows ( result ) ; } return [ ] ; } , async fillTransaction ( logger , transactions ) { if ( transactions . length > 0 ) { const transactionsMap = mapTransactionsToDict ( transactions ) ; return await populateTransactions ( databaseInstance , transactionsMap ) ; } logger . debug ( '[fillTransaction] Since no transactions were given, no inputs and outputs are looked for' ) ; return [ ] ; } , async findTransactionByHashAndBlock ( logger , hash , blockNumber , blockHash ) { logger . debug ( `[findTransactionByHashAndBlock] Parameters received for run query 
      blockNumber: ${ blockNumber }, blockHash: ${ blockHash }` ) ; const parameters = [ hexStringToBuffer ( hash ) , Number ( blockNumber ) , hexStringToBuffer ( blockHash ) ] ; logger . debug ( { parameters } , '[findTransactionByHashAndBlock] About to run findTransactionsByHashAndBlock query with parameters' ) ; const result = await databaseInstance . query ( Queries . findTransactionByHashAndBlock , parameters ) ; logger . debug ( `[findTransactionByHashAndBlock] Found ${ result . rowCount } transactions` ) ; if ( result . rows . length > 0 ) { const transactionsMap = mapTransactionsToDict ( parseTransactionRows ( result ) ) ; const [ transaction ] = await populateTransactions ( databaseInstance , transactionsMap ) ; return transaction || null ; } return null ; } , async findLatestBlockNumber ( logger ) { logger . debug ( '[findLatestBlockNumber] About to run findLatestBlockNumber query' ) ; const result = await databaseInstance . query ( Queries . findLatestBlockNumber ) ; const latestBlockNumber = result . rows [ 0 ] . blockHeight ; logger . debug ( `[findLatestBlockNumber] Latest block number is ${ latestBlockNumber }` ) ; return latestBlockNumber ; } , async getLinearFeeParameters ( logger ) { logger . debug ( '[getLinearFeeParameters] About to run getLinearFeeParameters query' ) ; const result = await databaseInstance . query ( Queries . findLatestMinFeeAAndMinFeeB ) ; return { minFeeA : result . rows [ 0 ] . min_fee_a , minFeeB : result . rows [ 0 ] . min_fee_b } ; } , async findGenesisBlock ( logger ) { logger . debug ( '[findGenesisBlock] About to run findGenesisBlock query' ) ; const result = await databaseInstance . query ( Queries . findGenesisBlock ) ; if ( result . rows . length === 1 ) { logger . debug ( '[findGenesisBlock] Genesis block was found' ) ; return { hash : hexFormatter ( result . rows [ 0 ] . hash ) , number : result . rows [ 0 ] . index } ; } logger . debug ( '[findGenesisBlock] Genesis block was not found' ) ; return null ; } , async findUtxoByAddressAndBlock ( logger , address , blockHash , currencies ) { const parameters = [ address , hexStringToBuffer ( blockHash ) ] ; logger . debug ( { address , blockHash } , '[findUtxoByAddressAndBlock] About to run findUtxoByAddressAndBlock query with parameters:' ) ; const currenciesIds = currencies ?. map ( c => ( { symbol : isEmptyHexString ( c . symbol ) ? '' : c . symbol , policy : c . metadata ?. policyId } ) ) ; const result = await databaseInstance . query ( Queries . findUtxoByAddressAndBlock ( currenciesIds ) , parameters ) ; logger . debug ( `[findUtxoByAddressAndBlock] Found ${ result . rowCount } utxos` ) ; return result . rows . map ( utxo => ( { value : utxo . value , transactionHash : hexFormatter ( utxo . txHash ) , index : utxo . index , name : utxo . name ? hexFormatter ( utxo . name ) : utxo . name , policy : utxo . policy ? hexFormatter ( utxo . policy ) : utxo . policy , quantity : utxo . quantity } ) ) ; } , async findMultiAssetByAddressAndBlock ( logger , address , blockHash ) { const parameters = [ address , hexStringToBuffer ( blockHash ) ] ; const balancesResult = await databaseInstance . query ( Queries . findMaBalanceByAddressAndBlock , parameters ) ; logger . debug ( `[findMultiAssetByAddressAndBlock] Found balances for ${ balancesResult . rowCount } multi assets` ) ; return balancesResult . rows . map ( multiAssetBalance => ( { name : multiAssetBalance ?. name && hexFormatter ( multiAssetBalance . name ) , policy : multiAssetBalance ?. policy && hexFormatter ( multiAssetBalance . policy ) , value : multiAssetBalance . value } ) ) ; } , async findBalanceByAddressAndBlock ( logger , address , blockHash ) { const parameters = [ address , hexStringToBuffer ( blockHash ) ] ; logger . debug ( { address , blockHash } , '[findBalanceByAddressAndBlock] About to run findBalanceByAddressAndBlock query with parameters:' ) ; const result = await databaseInstance . query ( Queries . findBalanceByAddressAndBlock , parameters ) ; logger . debug ( `[findBalanceByAddressAndBlock] Found a balance of ${ result . rows [ 0 ] . balance }` ) ; return result . rows [ 0 ] . balance ; } , async findTransactionsByFilters ( logger , conditions , limit , offset ) { logger . debug ( '[findTransactionsByConditions] Conditions received to run the query ' , { conditions } ) ; const { type , status , success , operator , currency } = conditions ; const coinIdentifier = coinIdentifierFormatter ( conditions ?. coin_identifier ?. identifier ) ; const transactionHash = conditions ?. transaction_identifier ?. hash ; const conditionsToQueryBy = { maxBlock : conditions ?. max_block , operator : operator ? operator : OperatorType . AND , type , coinIdentifier , status : validateAndGetStatus ( status , success ) , transactionHash , limit , offset , address : conditions . address || conditions . account_identifier ?. address } ; validateTransactionCoinMatch ( transactionHash , coinIdentifier ) ; validateAccountIdAddressMatch ( conditions . address , conditions . account_identifier ) ; type && validateOperationType ( type ) ; if ( currency && currency . symbol !== ADA ) { validateCurrencies ( [ currency ] ) ; const currencyId = { symbol : isEmptyHexString ( currency . symbol ) ? '' : currency . symbol , policy : currency . metadata ?. policyId } ; conditionsToQueryBy . currencyIdentifier = currencyId ; } const { data : dataQuery , count : totalCountQuery } = SearchQueries . generateComposedQuery ( conditionsToQueryBy ) ; logger . debug ( '[findTransactionsByConditions] About to search transactions' ) ; const parameters = [ ] ; if ( transactionHash || coinIdentifier ) parameters . push ( transactionHash ? hexStringToBuffer ( transactionHash ) : hexStringToBuffer ( coinIdentifier ! . hash ) ) ; const result = await databaseInstance . query ( dataQuery , parameters ) ; logger . debug ( `[findTransactionsByConditions] Found ${ result . rowCount } transactions` ) ; const totalCountResult = await databaseInstance . query ( totalCountQuery , parameters ) ; const totalCount = parseTotalCount ( totalCountResult ) ; logger . debug ( '[findTransactionsByConditions] Total count obtained ' , totalCount ) ; if ( result . rows . length > 0 ) { return { transactions : parseTransactionRows ( result ) , totalCount } ; } return { transactions : [ ] , totalCount } ; } } ) ;