import * as winston from "winston" ; import { Transaction } from "../models/TransactionModel" ; import { TransactionOperation } from "../models/TransactionOperationModel" ; import { removeScientificNotationFromNumbString } from "./Utils" ; import { Config } from "./Config" ; import * as Bluebird from "bluebird" ; import { IDecodedLog , IContract , ITransaction , IBlock , IExtractedTransaction , ISavedTransaction } from "./CommonInterfaces" ; const erc20abi = require ( "./contracts/Erc20Abi" ) ; const erc20ABIDecoder = require ( "abi-decoder" ) ; erc20ABIDecoder . addABI ( erc20abi ) ; export class TransactionParser { private OperationTypes = { Transfer : "Transfer" , } ; public parseTransactions ( blocks ) { if ( blocks . length === 0 ) return Promise . resolve ( ) ; const extractedTransactions = blocks . flatMap ( ( block ) => { return block . transactions . map ( ( tx ) => { return new Transaction ( this . extractTransactionData ( block , tx ) ) ; } ) ; } ) ; const txIDs = extractedTransactions . map ( ( tx ) => tx . _id ) ; return this . fetchTransactionReceipts ( txIDs ) . then ( ( receipts ) => { return this . mergeTransactionsAndReceipts ( extractedTransactions , receipts ) ; } ) . then ( ( transactions ) => { const bulkTransactions = Transaction . collection . initializeUnorderedBulkOp ( ) ; transactions . forEach ( ( transaction ) => bulkTransactions . find ( { _id : transaction . _id } ) . upsert ( ) . replaceOne ( transaction ) ) ; if ( bulkTransactions . length === 0 ) return Promise . resolve ( ) ; return bulkTransactions . execute ( ) . then ( ( bulkResult ) => { return Promise . resolve ( transactions ) ; } ) ; } ) ; } private mergeTransactionsAndReceipts ( transactions , receipts ) { const results = [ ] ; transactions . forEach ( ( transaction ) => { receipts . forEach ( ( receipt ) => { if ( transaction . _id == receipt . transactionHash ) { results . push ( this . mergeTransactionWithReceipt ( transaction , receipt ) ) ; } } ) ; } ) ; if ( transactions . length !== receipts . length ) { winston . error ( `Number of transactions not equal to number of receipts.` ) ; } return Promise . resolve ( results ) ; } private mergeTransactionWithReceipt ( transaction , receipt ) { const newTransaction = transaction ; newTransaction . gasUsed = receipt . gasUsed ; newTransaction . receipt = receipt ; newTransaction . contract = receipt . contractAddress ? receipt . contractAddress . toLowerCase ( ) : null ; newTransaction . error = receipt . status == ( "0x1" || "1" || 1 || null ) ? "" : "Error" ; return newTransaction ; } extractTransactionData ( block , transaction ) { const from = String ( transaction . from ) . toLowerCase ( ) ; const to = transaction . to === null ? "" : String ( transaction . to ) . toLowerCase ( ) ; const addresses = to ? [ from , to ] : [ from ] ; return { _id : String ( transaction . hash ) , blockNumber : Number ( transaction . blockNumber ) , timeStamp : String ( block . timestamp ) , nonce : Number ( transaction . nonce ) , from , to , value : String ( transaction . value ) , gas : String ( transaction . gas ) , gasPrice : String ( transaction . gasPrice ) , gasUsed : String ( 0 ) , input : String ( transaction . input ) , addresses } ; } public parseTransactionOperations ( transactions , contracts ) { if ( ! transactions || ! contracts ) return Promise . resolve ( ) ; return Bluebird . map ( transactions , ( transaction ) => { const decodedLogs = erc20ABIDecoder . decodeLogs ( transaction . receipt . logs ) . filter ( ( decodedLog ? ) => decodedLog ) ; if ( decodedLogs . length > 0 ) { return Bluebird . mapSeries ( decodedLogs , ( decodedLog , index ) => { if ( decodedLog . name === this . OperationTypes . Transfer ) { const contract = contracts . find ( ( contract ) => contract . address === decodedLog . address . toLowerCase ( ) ) ; if ( contract ) { const transfer = this . parseEventLog ( decodedLog ) ; return this . findOrCreateTransactionOperation ( transaction . _id , index , transfer . from , transfer . to , transfer . value , contract . _id ) ; } } } ) ; } } ) . catch ( ( err ) => { winston . error ( `Could not parse transaction operations with error: ${ err }` ) ; } ) ; } private createOperationObject ( transactionId , index , from , to , value , erc20ContractId ? ) { return { transactionId : this . getIndexedOperation ( transactionId , index ) , type : "token_transfer" , from : from . toLocaleLowerCase ( ) , to , value , contract : erc20ContractId } ; } private getIndexedOperation ( transactionId , index ) { return `${ transactionId }-${ index }` . toLowerCase ( ) ; } private parseEventLog ( eventLog ) { return { from : eventLog . events [ 0 ] . value , to : eventLog . events [ 1 ] . value , value : removeScientificNotationFromNumbString ( eventLog . events [ 2 ] . value ) , } ; } private findOrCreateTransactionOperation ( transactionId , index , from , to , value , erc20ContractId ? ) { const operation = this . createOperationObject ( transactionId , index , from , to , value , erc20ContractId ) ; const indexedOperation = this . getIndexedOperation ( transactionId , index ) ; return TransactionOperation . findOneAndUpdate ( { transactionId : indexedOperation } , operation , { upsert : true , new : true } ) . then ( ( operation ) => { return Transaction . findOneAndUpdate ( { _id : transactionId } , { $push : { operations : operation . _id , addresses : { $each : [ operation . to ] } } } ) . catch ( ( error ) => { winston . error ( `Could not update operation and address to transactionID ${ transactionId } with error: ${ error }` ) ; } ) ; } ) . catch ( ( error ) => { winston . error ( `Could not save transaction operation with error: ${ error }` ) ; } ) ; } private async fetchTransactionReceipts ( transactions ) { const batchLimit = 300 ; const chunk = ( list , size ) => list . reduce ( ( r , v ) => ( ! r . length || r [ r . length - 1 ] . length === size ? r . push ( [ v ] ) : r [ r . length - 1 ] . push ( v ) ) && r , [ ] ) ; const chunkTransactions = chunk ( transactions , batchLimit ) ; try { const receipts = await Bluebird . map ( chunkTransactions , ( chunk ) => { return new Promise ( ( resolve , reject ) => { let completed = false ; const chunkReceipts = [ ] ; const callback = ( err , receipt ) => { if ( completed ) return ; if ( err || ! receipt ) { completed = true ; reject ( err ) ; } chunkReceipts . push ( err ? null : receipt ) ; if ( chunkReceipts . length >= chunk . length ) { completed = true ; resolve ( chunkReceipts ) ; } } ; if ( chunk . length > 0 ) { const batch = new Config . web3 . BatchRequest ( ) ; chunk . forEach ( ( tx ) => { batch . add ( Config . web3 . eth . getTransactionReceipt . request ( tx , callback ) ) ; } ) ; batch . execute ( ) ; } else { resolve ( chunkReceipts ) ; } } ) ; } ) ; return [ ] . concat ( ... receipts ) ; } catch ( error ) { winston . error ( `Error getting transtaction receipt ` , error ) ; Promise . reject ( error ) ; } } static getTransactions ( blockNumber ) { return Transaction . find ( { blockNumber : { $eq : blockNumber } } ) . populate ( { path : "operations" , populate : { path : "contract" , model : "ERC20Contract" } } ) ; } static getTransactionsForAddress ( address ) { return Transaction . find ( { addresses : { "$in" : [ address ] } } ) . populate ( { path : "operations" , populate : { path : "contract" , model : "ERC20Contract" } } ) ; } }