"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const es6_promisify_1 = require("es6-promisify");
const configService_1 = require("./configService");
const loggingService_1 = require("./loggingService");
const pubSubEventService_1 = require("./pubSubEventService");
const utils_1 = require("./utils");
const utilsInternal_1 = require("./utilsInternal");
/* tslint:disable-next-line:no-var-requires */
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).
 */
class TransactionService extends pubSubEventService_1.PubSubEventService {
    /**
     * Generate a new invocation key.
     */
    static generateInvocationKey() {
        return Math.floor((Math.random() * Number.MAX_SAFE_INTEGER));
    }
    /**
     * Publish the `kickoff` event and return the payload that should be passed
     * to the ensuing calls to `publishTxLifecycleEvents`.
     *
     * @hidden - for internal use only
     * @param functionName Looks like [classname].[functionname], the function that is generating the transaction.
     * @param options Arbitrary object to pass in the callback for each event
     * @param txCount The expected number of transactions, will be included in the payload.
     */
    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;
    }
    /**
     * Send the given payload to subscribers of the given topic on `sent`, `mined` and `confirmed`.
     *
     * @hidden - for internal use only
     * @param eventContext array of TxEventSpec
     * @param tx the transaction id.
     * @param contract Truffle contract wrapper for the contract that is generating the transaction.
     */
    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) => {
            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) => {
                    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);
        });
    }
    static publishTxFailed(eventContext, atStage, error = new Error("Unspecified error"), tx, txReceipt) {
        TransactionService._publishTxEvent(eventContext.stack, tx, txReceipt, atStage, true, error);
    }
    /**
     * Return a new event stack with the given one pushed onto it.
     * Will take obj.txEventContext, else create a new one.
     *
     * Every transaction-generating function must invoke `newTxEventContext` to
     * define its own context. Whenever a transaction-generating function invokes
     * another transaction-generating function, it must add its `TxEventContext`
     * to the `options` object that it passes to the "nested" call
     * (see `TxGeneratingFunctionOptions`).
     *
     * Every transaction-generating function must assume that it may be a nested call,
     * that is, that the `options` object passed-into it may conform to `TxGeneratingFunctionOptions`
     *
     * @hidden - for internal use only
     * @param obj
     * @param eventSpec
     * @param addToObject True to clone obj and add the new txEventContext to it
     */
    static newTxEventContext(functionName, payload, obj) {
        const eventSpec = new TxEventSpec(functionName, payload);
        let eventContext = obj.txEventContext;
        if (!eventContext) {
            eventContext = new TxEventContext(payload.invocationKey, new Array());
        }
        else {
            // clone the incoming stack to avoid problems with re-entrancy
            eventContext = new TxEventContext(payload.invocationKey, [...eventContext.stack]);
        }
        // push the new context
        eventContext.stack.push(eventSpec);
        return eventContext;
    }
    /**
     * Returns a promise of a TransactionReceipt once the given transaction has been mined.
     *
     * See also [getMinedTransaction](/arc.js/api/classes/TransactionService#getMinedTransaction) and
     * [getTransactionDepth](/arc.js/api/classes/TransactionService#getTransactionDepth).
     *
     * @param txHash the transaction hash
     * @param contract Optional contract instance or contract name of the contract that generated the transaction.
     * Supply this if you want decoded events.
     * @param requiredDepth Optional minimum block depth required to resolve the promise.  Default is 0.
     * @returns Promise of `TransactionReceiptTruffle` if contract is given, else `TransactionReceipt`
     */
    static watchForMinedTransaction(txHash, contract = null, requiredDepth = 0) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    if (requiredDepth < 0) {
                        return reject(new Error(`TransactionService.watchForConfirmedTransaction: requiredDepth cannot be less then zero: ${requiredDepth}`));
                    }
                    const web3 = yield utils_1.Utils.getWeb3();
                    /**
                     * see if we already have it
                     */
                    let receipt = yield TransactionService.getMinedTransaction(txHash, contract, requiredDepth);
                    if (receipt) {
                        return resolve(receipt);
                    }
                    /**
                     * Fires on every new block
                     */
                    const filter = web3.eth.filter("latest");
                    let inside = false;
                    filter.watch((ex) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        if (!inside) { // to avoid re-entering
                            inside = true;
                            let stop = false;
                            if (!ex) {
                                receipt = yield TransactionService.getMinedTransaction(txHash, contract, requiredDepth);
                                if (receipt) {
                                    stop = true;
                                }
                            }
                            else {
                                loggingService_1.LoggingService.error(`TransactionService.watchForMinedTransaction: an error occurred: ${ex}`);
                                stop = true;
                            }
                            if (stop) {
                                yield utilsInternal_1.UtilsInternal.stopWatchingAsync(filter).then(() => {
                                    if (receipt) {
                                        return resolve(receipt);
                                    }
                                    else {
                                        return reject(ex);
                                    }
                                });
                            }
                            inside = false;
                        }
                    }));
                }
                catch (ex) {
                    return reject(ex);
                }
            }));
        });
    }
    /**
     * Returns a promise of a TransactionReceipt once the given transaction has been confirmed
     * according to the optional `requiredDepth`.
     *
     * See also [getConfirmedTransaction](/arc.js/api/classes/TransactionService#getConfirmedTransaction) and
     * [getTransactionDepth](/arc.js/api/classes/TransactionService#getTransactionDepth).
     *
     * @param txHash The transaction hash to watch
     * @param contract Optional contract instance or contract name of the contract that generated the transaction.
     * Supply this if you want decoded events.
     * @param requiredDepth Optional minimum block depth required to resolve the promise.
     * Default comes from the `ConfigService`.
     */
    static watchForConfirmedTransaction(txHash, contract = null, requiredDepth) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    requiredDepth = yield TransactionService.getDefaultDepth(requiredDepth);
                    return resolve(yield TransactionService.watchForMinedTransaction(txHash, contract, requiredDepth));
                }
                catch (ex) {
                    return reject(ex);
                }
            }));
        });
    }
    /**
     * Returns a promise of the number of blocks that have been added to the chain since
     * the given transaction appeared. Use this to decide whether a transaction is
     * sufficiently secure (confirmed).
     *
     * See also [getConfirmedTransaction](/arc.js/api/classes/TransactionService#getConfirmedTransaction)
     * and [watchForConfirmedTransaction](/arc.js/api/classes/TransactionService#watchForConfirmedTransaction).
     * @param tx txHash or TransactionReceipt
     * @returns Promise of the depth or -1 if the transaction cannot be found
     */
    static getTransactionDepth(tx) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const web3 = yield utils_1.Utils.getWeb3();
            const lastBlockNum = yield utilsInternal_1.UtilsInternal.lastBlockNumber();
            let receipt;
            if (typeof tx === "string") {
                receipt = (yield es6_promisify_1.promisify((innerCallback) => {
                    web3.eth.getTransactionReceipt(tx, innerCallback);
                })());
            }
            else {
                receipt = tx;
            }
            if (receipt) {
                const txBlockNum = receipt.blockNumber;
                return lastBlockNum - txBlockNum;
            }
            else {
                return Promise.resolve(-1);
            }
        });
    }
    /**
     * Returns a promise of a TransactionReceipt for a mined transaction, or null if it hasn't yet been mined.
     * @param txHash
     * @param requiredDepth Optional minimum block depth required to resolve the promise.  Default is 0.
     * @param contract Optional contract instance or contract name of the contract that generated the transaction.
     * Supply this if you want decoded events.
     * @returns Promise of `TransactionReceiptTruffle` if contract is given, else `TransactionReceipt`,
     * or null if not found.
     */
    static getMinedTransaction(txHash, contract = null, requiredDepth = 0) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const web3 = yield utils_1.Utils.getWeb3();
            return es6_promisify_1.promisify((innerCallback) => {
                web3.eth.getTransactionReceipt(txHash, innerCallback);
            })()
                .then((receipt) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                const depth = yield TransactionService.getTransactionDepth(receipt);
                // blockNumber should always be set, but just in case...
                if ((receipt && !receipt.blockNumber) || (depth < requiredDepth)) {
                    return null;
                }
                else {
                    if (contract) {
                        return yield TransactionService.toTxTruffle(receipt, contract);
                    }
                    else {
                        return receipt;
                    }
                }
            }));
        });
    }
    /**
     * Returns a promise of a TransactionReceipt for a confirmed transaction, or null if it hasn't yet been confirmed
     * according to the requiredDepth.
     * @param txHash
     * @param requiredDepth Optional minimum block depth required to resolve the promise.
     * Default comes from the `ConfigService`.
     * @param contract Optional contract instance or contract name of the contract that generated the transaction.
     * Supply this if you want decoded events.
     */
    static getConfirmedTransaction(txHash, contract = null, requiredDepth) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            requiredDepth = yield TransactionService.getDefaultDepth(requiredDepth);
            return TransactionService.getMinedTransaction(txHash, contract, requiredDepth);
        });
    }
    /**
     * Convert a mined TransactionReceipt to a TransactionReceiptTruffle with more readable logs
     * such as are produced by truffle (see `DecodedLogEntry`).
     *
     * @hidden - for internal use only
     *
     * @param txReceipt The mined tx
     * @param contract The truffle contract that generated the tx
     */
    static toTxTruffle(txReceipt, contract) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let contractInstance;
            if (typeof contract === "string") {
                // Can't use WrapperService here without causing circular dependencies.
                contractInstance = yield (yield utils_1.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.receipt) {
                // then already done
                return;
            }
            const logs = txReceipt.logs.map((log) => {
                const logABI = events[log.topics[0]];
                if (logABI == null) {
                    return null;
                }
                const copy = Object.assign({}, log);
                const partialABI = (fullABI, indexed) => {
                    const inputs = fullABI.inputs.filter((i) => {
                        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) => 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) => {
                    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];
                    // We have BN. Convert it to BigNumber
                    if (val.constructor.isBN) {
                        copy.args[key] = contractInstance.constructor.web3.toBigNumber("0x" + val.toString(16));
                    }
                });
                delete copy.data;
                delete copy.topics;
                return copy;
            }).filter((log) => {
                return log != null;
            });
            return {
                logs,
                receipt: txReceipt,
                transactionHash: txReceipt.transactionHash,
            };
        });
    }
    /**
     * Returns a value from the given transaction log.
     * Undefined if not found for any reason.
     *
     * @param tx The transaction
     * @param arg The name of the property whose value we wish to return from the args object:
     *  tx.logs[index].args[argName]
     * @param eventName Overrides index, identifies which log,
     *  where tx.logs[n].event === eventName
     * @param index Identifies which log when eventName is not given
     */
    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) {
            // TODO: log "getValueFromLogs: Transaction has no logs");
            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") {
                // TODO: log  `getValueFromLogs: There is no event logged with eventName ${eventName}`
                return undefined;
            }
        }
        else if (typeof index === "undefined") {
            index = tx.logs.length - 1;
        }
        const result = tx.logs[index].args[arg];
        if (!result) {
            // TODO: log  `getValueFromLogs: This log does not seem to have a field "${arg}": ${tx.logs[index].args}`
            return undefined;
        }
        return result;
    }
    /**
     * Returns the default value for required block depth defined for the current network
     * in the Arc.js global configuration
     * "[txDepthRequiredForConfirmation](/Configuration.md#txDepthRequiredForConfirmation)".
     * @param requiredDepth Overrides the default if given
     */
    static getDefaultDepth(requiredDepth) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (typeof requiredDepth === "undefined") {
                const requiredDepths = configService_1.ConfigService.get("txDepthRequiredForConfirmation");
                const networkName = yield utils_1.Utils.getNetworkName();
                requiredDepth = requiredDepths[networkName.toLowerCase()];
                if (typeof requiredDepth === "undefined") {
                    requiredDepth = requiredDepths.default;
                }
            }
            return requiredDepth;
        });
    }
    static createPayload(functionName, options, txCount) {
        const payload = {
            functionName,
            options,
            tx: null,
            txCount,
            txReceipt: null,
            txStage: TransactionStage.kickoff,
        };
        payload.invocationKey = options.txEventContext ?
            options.txEventContext.invocationKey : TransactionService.generateInvocationKey();
        return payload;
    }
    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_1.PubSubEventService.publish(fullTopic, payload);
        }
    }
    static topicBaseFromFunctionName(functionName) {
        return `TxTracking.${functionName}`;
    }
}
exports.TransactionService = TransactionService;
var TransactionStage;
(function (TransactionStage) {
    TransactionStage[TransactionStage["kickoff"] = 0] = "kickoff";
    TransactionStage[TransactionStage["sent"] = 1] = "sent";
    TransactionStage[TransactionStage["mined"] = 2] = "mined";
    TransactionStage[TransactionStage["confirmed"] = 3] = "confirmed";
})(TransactionStage = exports.TransactionStage || (exports.TransactionStage = {}));
/**
 * @hidden - for internal use only
 */
class TxEventContext {
    constructor(invocationKey, stack) {
        this.invocationKey = invocationKey;
        this.stack = stack;
    }
}
exports.TxEventContext = TxEventContext;
/**
 * Arc.js inserts this into the `options` object that the
 * user has passed into the transaction-generating wrapper function.
 *
 * @hidden - for internal use only
 */
class TxEventSpec {
    constructor(functionName, payload) {
        this.functionName = functionName;
        this.payload = payload;
    }
}
exports.TxEventSpec = TxEventSpec;
//# sourceMappingURL=transactionService.js.map