"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.initContractEventGateway = exports.queryAllReadonlyContracts = exports.createDecoratedContract = exports.waitForTx = void 0;
const utils_1 = require("@streamr/utils");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const shuffle_1 = __importDefault(require("lodash/shuffle"));
const without_1 = __importDefault(require("lodash/without"));
const p_limit_1 = __importDefault(require("p-limit"));
const promises_1 = require("./promises");
async function waitForTx(txToSubmit) {
    const tx = await txToSubmit;
    return tx.wait();
}
exports.waitForTx = waitForTx;
const isTransaction = (returnValue) => {
    return (returnValue.wait !== undefined && (typeof returnValue.wait === 'function'));
};
const createLogger = (eventEmitter, loggerFactory) => {
    const logger = loggerFactory.createLogger(module);
    eventEmitter.on('onMethodExecute', (methodName) => {
        logger.debug('Execute method', { methodName });
    });
    eventEmitter.on('onTransactionSubmit', (methodName, tx) => {
        logger.debug('Submit transaction', {
            method: methodName,
            tx: tx.hash,
            to: tx.to,
            nonce: tx.nonce,
            gasLimit: tx.gasLimit.toNumber(),
            gasPrice: tx.gasPrice?.toNumber()
        });
    });
    eventEmitter.on('onTransactionConfirm', (methodName, tx, receipt) => {
        logger.debug('Received transaction confirmation', {
            method: methodName,
            tx: tx.hash,
            block: receipt.blockNumber,
            confirmations: receipt.confirmations,
            gasUsed: receipt.gasUsed.toNumber(),
            events: (receipt.events || []).map((e) => e.event)
        });
    });
};
const withErrorHandling = async (execute, methodName, action) => {
    try {
        return await execute();
    }
    catch (e) {
        const suffixes = (0, without_1.default)(['reason', 'code'].map((field) => (e[field] !== undefined ? `${field}=${e[field]}` : undefined)), undefined);
        const wrappedError = new Error(`Error while ${action} contract call "${methodName}"${(suffixes.length > 0) ? ', ' + suffixes.join(', ') : ''}`);
        // @ts-expect-error unknown property
        wrappedError.reason = e;
        throw wrappedError;
    }
};
const createWrappedContractMethod = (originalMethod, methodName, eventEmitter, concurrencyLimit) => {
    return async (...args) => {
        const returnValue = await withErrorHandling(() => concurrencyLimit(() => {
            eventEmitter.emit('onMethodExecute', methodName);
            return originalMethod(...args);
        }), methodName, 'executing');
        if (isTransaction(returnValue)) {
            const tx = returnValue;
            const originalWaitMethod = tx.wait;
            tx.wait = async (confirmations) => {
                const receipt = await withErrorHandling(() => originalWaitMethod(confirmations), methodName, 'waiting transaction for');
                eventEmitter.emit('onTransactionConfirm', methodName, tx, receipt);
                return receipt;
            };
            eventEmitter.emit('onTransactionSubmit', methodName, tx);
        }
        return returnValue;
    };
};
/**
 * Adds error handling, logging and limits concurrency.
 *
 * You can use the decorated contract normally, e.g.:
 *     const tx = await contract.createFoobar(123)
 *     return await tx.wait()
 * or
 *     await contract.getFoobar(456)
 */
const createDecoratedContract = (contract, contractName, loggerFactory, maxConcurrentCalls) => {
    const eventEmitter = new eventemitter3_1.default();
    const methods = {};
    const concurrencyLimit = (0, p_limit_1.default)(maxConcurrentCalls);
    /*
     * Wrap each contract function. We read the list of functions from contract.functions, but
     * actually delegate each method to contract[methodName]. Those methods are almost identical
     * to contract.functions[methodName] methods. The major difference is the way of handling
     * single-value results: the return type of contract.functions[methodName] is always
     * Promise<Result> (see https://docs.ethers.io/v5/api/contract/contract/#Contract--readonly)
     */
    Object.keys(contract.functions).forEach((methodName) => {
        methods[methodName] = createWrappedContractMethod(contract[methodName], `${contractName}.${methodName}`, eventEmitter, concurrencyLimit);
    });
    createLogger(eventEmitter, loggerFactory);
    const result = {
        eventEmitter
    };
    function getAllPropertyNames(obj) {
        const proto = Object.getPrototypeOf(obj);
        const inherited = (proto) ? getAllPropertyNames(proto) : [];
        return [...new Set(Object.getOwnPropertyNames(obj).concat(inherited))];
    }
    // copy own properties and inherited properties (e.g. contract.removeAllListeners)
    // eslint-disable-next-line no-prototype-builtins
    for (const key of getAllPropertyNames(contract)) {
        result[key] = methods[key] !== undefined ? methods[key] : contract[key];
    }
    return result;
};
exports.createDecoratedContract = createDecoratedContract;
const queryAllReadonlyContracts = (call, contracts) => {
    return (0, promises_1.tryInSequence)((0, shuffle_1.default)(contracts).map((contract) => {
        return () => call(contract);
    }));
};
exports.queryAllReadonlyContracts = queryAllReadonlyContracts;
const initContractEventGateway = (opts) => {
    const logger = opts.loggerFactory.createLogger(module);
    (0, utils_1.initEventGateway)(opts.targetName, (emit) => {
        const listener = (...args) => {
            let targetEvent;
            try {
                targetEvent = opts.transformation(...args);
            }
            catch (err) {
                logger.debug('Skip emit event', {
                    eventName: opts.targetName,
                    reason: err?.message
                });
                return;
            }
            emit(targetEvent);
        };
        opts.sourceEmitter.on(opts.sourceName, listener);
        return listener;
    }, (listener) => {
        opts.sourceEmitter.off(opts.sourceName, listener);
    }, opts.targetEmitter);
};
exports.initContractEventGateway = initContractEventGateway;
//# sourceMappingURL=contract.js.map