"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.createDecoratedContract = void 0;
exports.waitForTx = waitForTx;
const utils_1 = require("@streamr/utils");
const ethers_1 = require("ethers");
const eventemitter3_1 = __importDefault(require("eventemitter3"));
const without_1 = __importDefault(require("lodash/without"));
const p_limit_1 = __importDefault(require("p-limit"));
async function waitForTx(txToSubmit) {
    const tx = await txToSubmit;
    return tx.wait(); // cannot be null unless arg confirmations set to 0
}
const isTransactionResponse = (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,
            gasPrice: tx.gasPrice
        });
    });
    eventEmitter.on('onTransactionConfirm', (methodName, receipt) => {
        logger.debug('Received transaction confirmation', {
            method: methodName,
            tx: receipt?.hash,
            block: receipt?.blockNumber,
            confirmations: receipt?.confirmations,
            gasUsed: receipt?.gasUsed,
            events: receipt?.logs ?? []
        });
    });
};
const withErrorHandling = async (execute, methodName, action) => {
    try {
        return await execute();
    }
    catch (e) {
        const suffixes = (0, without_1.default)(
        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
        ['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 = (contract, contractName, methodName, eventEmitter, concurrencyLimit) => {
    const originalMethod = contract[methodName];
    const methodFullName = `${contractName}.${methodName}`;
    const fn = async (...args) => {
        const returnValue = await withErrorHandling(() => concurrencyLimit(() => {
            eventEmitter.emit('onMethodExecute', methodFullName);
            return originalMethod(...args);
        }), methodFullName, 'executing');
        if (isTransactionResponse(returnValue)) {
            eventEmitter.emit('onTransactionSubmit', methodFullName, returnValue);
            const originalWait = returnValue.wait.bind(returnValue);
            returnValue.wait = async (confirmations, timeout) => {
                const receipt = await withErrorHandling(() => originalWait(confirmations, timeout), methodName, 'waiting transaction for');
                eventEmitter.emit('onTransactionConfirm', methodFullName, receipt);
                return receipt;
            };
        }
        return returnValue;
    };
    // TODO add also other methods in the future if needed:
    // https://docs.ethers.org/v6/api/contract/#BaseContractMethod
    fn.estimateGas = (...args) => originalMethod.estimateGas(...args);
    return fn;
};
/**
 * 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 concurrencyLimit = (0, p_limit_1.default)(maxConcurrentCalls);
    const decoratedContract = {
        eventEmitter,
        getAddress: () => contract.getAddress(),
        // TODO implement also other generic contract methods in the future if needed
        on: (eventName, listener) => {
            contract.on(eventName, listener);
        },
        off: (eventName, listener) => {
            contract.off(eventName, listener);
        }
    };
    /*
     * 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.org/v6/api/contract/#BaseContract)
     */
    const methodNames = contract.interface.fragments.filter((f) => ethers_1.FunctionFragment.isFunction(f)).map((f) => f.name);
    methodNames.forEach((methodName) => {
        decoratedContract[methodName] = createWrappedContractMethod(contract, contractName, methodName, eventEmitter, concurrencyLimit);
    });
    createLogger(eventEmitter, loggerFactory);
    return decoratedContract;
};
exports.createDecoratedContract = createDecoratedContract;
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.error('Skip emit event', {
                    eventName: opts.targetName,
                    reason: err?.message
                });
                return;
            }
            emit(targetEvent);
        };
        opts.sourceEmitter.on({
            onEvent: listener,
            ...opts.sourceDefinition
        });
        return listener;
    }, (listener) => {
        opts.sourceEmitter.off({
            onEvent: listener,
            ...opts.sourceDefinition
        });
    }, opts.targetEmitter);
};
exports.initContractEventGateway = initContractEventGateway;
//# sourceMappingURL=contract.js.map