import { promisify } from "es6-promisify" ; import { DecodedLogEntry , LogEntry , TransactionReceipt } from "web3" ; import { Hash } from "./commonTypes" ; import { ConfigService } from "./configService" ; import { LoggingService } from "./loggingService" ; import { PubSubEventService } from "./pubSubEventService" ; import { Utils } from "./utils" ; import { UtilsInternal } from "./utilsInternal" ; const ethJSABI = require ( "ethjs-abi" ) ; /**
 * Enables you to track the completion of transactions triggered by Arc.js functions.
 * You can subscribe to events that tell you how many transactions are anticipated when
 * the transactions have completed.  For more information, see [Tracking Transactions](/Transactions.md).
 */ export class TransactionService extends PubSubEventService { /**
     * Generate a new invocation key.
     */ public static generateInvocationKey ( ) { return Math . floor ( ( Math . random ( ) * Number . MAX_SAFE_INTEGER ) ) ; } hidden param functionName param options param txCount public static publishKickoffEvent ( functionName , options , txCount ) { const payload = TransactionService . createPayload ( functionName , options , txCount ) ; /**
         * publish the `kickoff` event
         */ TransactionService . _publishTxEvent ( [ new TxEventSpec ( functionName , payload ) ] , null , null , TransactionStage . kickoff ) ; return payload ; } hidden param eventContext param tx param contract public static publishTxLifecycleEvents ( eventContext , tx , contract ) { TransactionService . _publishTxEvent ( eventContext . stack , tx , null , TransactionStage . sent ) ; /**
         * We are at the base context and should start watching for the mined and confirmed transaction stages.
         */ TransactionService . watchForMinedTransaction ( tx , contract ) . then ( ( txReceiptMined ) : void => { if ( txReceiptMined . receipt . status !== "0x1" ) { TransactionService . publishTxFailed ( eventContext , TransactionStage . mined , new Error ( "Transaction status is 0" ) , tx , txReceiptMined ) ; } else { TransactionService . _publishTxEvent ( eventContext . stack , tx , txReceiptMined , TransactionStage . mined ) ; /**
                 * now start watching for confirmation
                 */ TransactionService . watchForConfirmedTransaction ( tx , contract ) . then ( ( txReceiptConfirmed ) : void => { if ( txReceiptConfirmed . receipt . status !== "0x1" ) { TransactionService . publishTxFailed ( eventContext , TransactionStage . confirmed , new Error ( "Transaction status is 0" ) , tx , txReceiptConfirmed ) ; } else { TransactionService . _publishTxEvent ( eventContext . stack , tx , txReceiptConfirmed , TransactionStage . confirmed ) ; } } ) . catch ( ( ex ) => { TransactionService . publishTxFailed ( eventContext , TransactionStage . confirmed , ex , tx , txReceiptMined ) ; } ) ; } } ) . catch ( ( ex ) => { TransactionService . publishTxFailed ( eventContext , TransactionStage . mined , ex , tx ) ; } ) ; } public static publishTxFailed ( eventContext , atStage , error = new Error ( "Unspecified error" ) , tx ? , txReceipt ? ) { TransactionService . _publishTxEvent ( eventContext . stack , tx , txReceipt , atStage , true , error ) ; } hidden param obj param eventSpec param addToObject public static newTxEventContext ( functionName , payload , obj ) { const eventSpec = new TxEventSpec ( functionName , payload ) ; let eventContext = obj . txEventContext ; if ( ! eventContext ) { eventContext = new TxEventContext ( payload . invocationKey , new Array < TxEventSpec > ( ) ) ; } else { eventContext = new TxEventContext ( payload . invocationKey , [ ... eventContext . stack ] ) ; } eventContext . stack . push ( eventSpec ) ; return eventContext ; } param txHash param contract param requiredDepth returns public static async watchForMinedTransaction ( txHash , contract = null , requiredDepth = 0 ) { return new Promise ( async ( resolve , reject ) : Promise < void > => { try { if ( requiredDepth < 0 ) { return reject ( new Error ( `TransactionService.watchForConfirmedTransaction: requiredDepth cannot be less then zero: ${ requiredDepth }` ) ) ; } const web3 = await Utils . getWeb3 ( ) ; /**
                 * see if we already have it
                 */ let receipt = await TransactionService . getMinedTransaction ( txHash , contract , requiredDepth ) ; if ( receipt ) { return resolve ( receipt ) ; } /**
                 * Fires on every new block
                 */ const filter = web3 . eth . filter ( "latest" ) ; filter . watch ( async ( ex ) : Promise < void > => { if ( ! ex ) { receipt = await TransactionService . getMinedTransaction ( txHash , contract , requiredDepth ) ; if ( receipt ) { await UtilsInternal . stopWatchingAsync ( filter ) . then ( ( ) => { return resolve ( receipt ) ; } ) ; } } else { LoggingService . error ( `TransactionService.watchForMinedTransaction: an error occurred: ${ ex }` ) ; return reject ( ex ) ; } } ) ; } catch ( ex ) { return reject ( ex ) ; } } ) ; } param txHash param contract param requiredDepth public static async watchForConfirmedTransaction ( txHash , contract = null , requiredDepth ? ) { return new Promise ( async ( resolve , reject ) : Promise < void > => { try { requiredDepth = await TransactionService . getDefaultDepth ( requiredDepth ) ; return resolve ( await TransactionService . watchForMinedTransaction ( txHash , contract , requiredDepth ) ) ; } catch ( ex ) { return reject ( ex ) ; } } ) ; } param tx returns public static async getTransactionDepth ( tx ) { const web3 = await Utils . getWeb3 ( ) ; const lastBlockNum = await UtilsInternal . lastBlockNumber ( ) ; let receipt ; if ( typeof tx === "string" ) { receipt = ( await promisify ( ( innerCallback ) => { web3 . eth . getTransactionReceipt ( tx , innerCallback ) ; } ) ( ) ) as TransactionReceipt | null ; } else { receipt = tx ; } if ( receipt ) { const txBlockNum = receipt . blockNumber ; return lastBlockNum - txBlockNum ; } else { return Promise . resolve ( - 1 ) ; } } param txHash param requiredDepth param contract returns public static async getMinedTransaction ( txHash , contract = null , requiredDepth = 0 ) { const web3 = await Utils . getWeb3 ( ) ; return promisify ( ( innerCallback ) => { web3 . eth . getTransactionReceipt ( txHash , innerCallback ) ; } ) ( ) . then ( async ( receipt ) => { const depth = await TransactionService . getTransactionDepth ( receipt ) ; if ( ( receipt && ! receipt . blockNumber ) || ( depth < requiredDepth ) ) { return null ; } else { if ( contract ) { return await TransactionService . toTxTruffle ( receipt , contract ) ; } else { return receipt ; } } } ) as Promise < TransactionReceipt | null > ; } param txHash param requiredDepth param contract public static async getConfirmedTransaction ( txHash , contract = null , requiredDepth ? ) { requiredDepth = await TransactionService . getDefaultDepth ( requiredDepth ) ; return TransactionService . getMinedTransaction ( txHash , contract , requiredDepth ) ; } hidden param txReceipt param contract public static async toTxTruffle ( txReceipt , contract ) { let contractInstance ; if ( typeof contract === "string" ) { contractInstance = await ( await Utils . requireContract ( contract ) ) . deployed ( ) ; if ( ! contractInstance ) { throw new Error ( `TransactionService.toTxTruffle: can't find contract ${ contract }` ) ; } } else { contractInstance = contract ; } const events = contractInstance . constructor . events ; if ( ( txReceipt as any ) . receipt ) { return ; } const logs = txReceipt . logs . map ( ( log ) : DecodedLogEntry < any > => { const logABI = events [ log . topics [ 0 ] ] ; if ( logABI == null ) { return null ; } const copy = Object . assign ( { } , log ) as any ; const partialABI = ( fullABI , indexed ) : any => { const inputs = fullABI . inputs . filter ( ( i ) : boolean => { return i . indexed === indexed ; } ) ; const partial = { anonymous : fullABI . anonymous , inputs , name : fullABI . name , type : fullABI . type , } ; return partial ; } ; const argTopics = logABI . anonymous ? copy . topics : copy . topics . slice ( 1 ) ; const indexedData = "0x" + argTopics . map ( ( topics ) : any => topics . slice ( 2 ) ) . join ( "" ) ; const indexedParams = ethJSABI . decodeEvent ( partialABI ( logABI , true ) , indexedData ) ; const notIndexedData = copy . data ; const notIndexedParams = ethJSABI . decodeEvent ( partialABI ( logABI , false ) , notIndexedData ) ; copy . event = logABI . name ; copy . args = logABI . inputs . reduce ( ( acc , current ) : any => { let val = indexedParams [ current . name ] ; if ( val === undefined ) { val = notIndexedParams [ current . name ] ; } acc [ current . name ] = val ; return acc ; } , { } ) ; Object . keys ( copy . args ) . forEach ( ( key ) => { const val = copy . args [ key ] ; if ( val . constructor . isBN ) { copy . args [ key ] = contractInstance . constructor . web3 . toBigNumber ( "0x" + val . toString ( 16 ) ) ; } } ) ; delete copy . data ; delete copy . topics ; return copy as any ; } ) . filter ( ( log ) : boolean => { return log != null ; } ) ; return { logs , receipt : txReceipt , transactionHash : txReceipt . transactionHash , } ; } param tx param arg param eventName param index public static getValueFromLogs ( tx , arg , eventName = null , index = 0 ) { /**
         *
         * tx is an object with the following values:
         *
         * tx.tx      => transaction hash, string
         * tx.logs    => array of decoded events that were triggered within this transaction
         * tx.receipt => transaction receipt object, which includes gas used
         *
         * tx.logs look like this:
         *
         * [ { logIndex: 13,
         *     transactionIndex: 0,
         *     transactionHash: "0x999e51b4124371412924d73b60a0ae1008462eb367db45f8452b134e5a8d56c8",
         *     blockHash: "0xe35f7c374475a6933a500f48d4dfe5dce5b3072ad316f64fbf830728c6fe6fc9",
         *     blockNumber: 294,
         *     address: "0xd6a2a42b97ba20ee8655a80a842c2a723d7d488d",
         *     type: "mined",
         *     event: "NewOrg",
         *     args: { _avatar: "0xcc05f0cde8c3e4b6c41c9b963031829496107bbb" } } ]
         */ if ( ! tx . logs || ! tx . logs . length ) { return undefined ; } if ( eventName && ( eventName . length ) ) { for ( let i = 0 ; i < tx . logs . length ; i ++ ) { if ( tx . logs [ i ] . event === eventName ) { index = i ; break ; } } if ( typeof index === "undefined" ) { return undefined ; } } else if ( typeof index === "undefined" ) { index = tx . logs . length - 1 ; } const result = tx . logs [ index ] . args [ arg ] ; if ( ! result ) { return undefined ; } return result ; } param requiredDepth public static async getDefaultDepth ( requiredDepth ? ) { if ( typeof requiredDepth === "undefined" ) { const requiredDepths = ConfigService . get ( "txDepthRequiredForConfirmation" ) ; const networkName = await Utils . getNetworkName ( ) ; requiredDepth = requiredDepths [ networkName . toLowerCase ( ) ] ; if ( typeof requiredDepth === "undefined" ) { requiredDepth = requiredDepths . default ; } } return requiredDepth ; } private static createPayload ( functionName , options , txCount ) { const payload = { functionName , options , tx : null , txCount , txReceipt : null , txStage : TransactionStage . kickoff , } as Partial < TransactionReceiptsEventInfo > ; payload . invocationKey = options . txEventContext ? options . txEventContext . invocationKey : TransactionService . generateInvocationKey ( ) ; return payload as TransactionReceiptsEventInfo ; } private static _publishTxEvent ( eventStack , tx , txReceipt = null , txStage , failed = false , error = new Error ( "Unspecified error" ) ) { for ( let i = eventStack . length - 1 ; i >= 0 ; -- i ) { const eventSpec = eventStack [ i ] ; const functionName = eventSpec . functionName ; const baseTopic = TransactionService . topicBaseFromFunctionName ( functionName ) ; const fullTopic = `${ baseTopic }.${ TransactionStage [ txStage ] }${ failed ? ".failed" : "" }` ; /**
             * Clone to handle re-entrancy and ensure that recipients of the event can
             * rely on receiving a distinct payload for each event. This can be particularly
             * needed with fast networks like ganache, but also if the consumer is
             * saving these payloads on each stage of the lifecycle and expecting them
             * to be distinct.
             */ const payload = Object . assign ( { } , eventSpec . payload ) ; payload . tx = tx ; payload . txReceipt = txReceipt ; payload . txStage = txStage ; if ( failed ) { payload . error = error ; } PubSubEventService . publish ( fullTopic , payload ) ; } } private static topicBaseFromFunctionName ( functionName ) { return `TxTracking.${ functionName }` ; } } export enum TransactionStage { kickoff , sent , mined , confirmed } /**
 * Information supplied to the event callback when the event is published. Otherwise
 * known as the "payload".
 */ export interface TransactionReceiptsEventInfo { /**
     * A value that is unique to the invocation of the function that is generating
     * the transaction. It is useful for grouping events by a single function invocation.
     */ invocationKey : number ; /**
     * The options that were passed to the function that is generating the transaction, if any.
     * In most cases this will have default values filled in.
     */ options ? : any ; /**
     * The name of the function that is generating the transactions and these events.
     */ functionName : string ; /**
     * The transaction hash id.  Note that the transaction may not necessarily have completed successfully
     * in the case of errors or rejection.
     * Will be null in `kickoff` events.
     */ tx : Hash | null ; /**
     * The `TransactionReceiptTruffle` once the transaction has been mined.
     * Will be null in `kickoff` and `sent` events.
     */ txReceipt : TransactionReceiptTruffle | null ; /**
     * The total expected number of transactions.
     */ txCount : number ; /**
     * Stage of the transaction.  Can be `kickoff`, `sent`, `mined` or `confirmed`.
     */ txStage : TransactionStage ; /**
     * Error when a failure has occured.  Supplied by the ".failed" events.
     */ error ? : Error ; } hidden export class TxEventContext { constructor ( public invocationKey , public stack ) { } } hidden export class TxEventSpec { constructor ( public functionName , public payload ) { } } hidden export interface TxGeneratingFunctionOptions { txEventContext ? : TxEventContext ; } /**
 * The bundle of logs, TransactionReceipt and other information as returned by Truffle after invoking
 * a contract function that causes a transaction.
 */ export interface TransactionReceiptTruffle { logs : Array < any > ; receipt : TransactionReceipt ; transactionHash : Hash ; }