"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EthereumChainService = exports.waitForTransaction = exports.BIG_GAS_PRICE = exports.BIG_GAS_LIMIT = exports.GAS_BUMP_PERCENT = exports.CONFIRMATION_TIMEOUT = void 0;
const vector_types_1 = require("@connext/vector-types");
const vector_utils_1 = require("@connext/vector-utils");
const bignumber_1 = require("@ethersproject/bignumber");
const contracts_1 = require("@ethersproject/contracts");
const wallet_1 = require("@ethersproject/wallet");
const p_queue_1 = __importDefault(require("p-queue"));
const constants_1 = require("@ethersproject/constants");
const evt_1 = require("evt");
const uuid_1 = require("uuid");
const artifacts_1 = require("../artifacts");
const ethReader_1 = require("./ethReader");
const utils_1 = require("ethers/lib/utils");
exports.CONFIRMATION_TIMEOUT = 45000;
exports.GAS_BUMP_PERCENT = 20;
exports.BIG_GAS_LIMIT = bignumber_1.BigNumber.from(2000000);
exports.BIG_GAS_PRICE = utils_1.parseUnits("1500", "gwei");
const waitForTransaction = async (provider, transactionHash, confirmations, timeout) => {
    try {
        const receipt = await provider.waitForTransaction(transactionHash, confirmations, timeout);
        if (!receipt) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TransferNotFound, { receipt }));
        }
        if (receipt.status === 0) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TxReverted, { receipt }));
        }
        return vector_types_1.Result.ok(receipt);
    }
    catch (e) {
        return vector_types_1.Result.fail(e);
    }
};
exports.waitForTransaction = waitForTransaction;
class EthereumChainService extends ethReader_1.EthereumChainReader {
    constructor(store, chainProviders, signer, log, defaultRetries = 3) {
        super(chainProviders, log);
        this.store = store;
        this.defaultRetries = defaultRetries;
        this.signers = new Map();
        this.queue = new p_queue_1.default({ concurrency: 1 });
        this.evts = Object.assign(Object.assign({}, this.disputeEvts), { [vector_types_1.ChainServiceEvents.TRANSACTION_SUBMITTED]: new evt_1.Evt(), [vector_types_1.ChainServiceEvents.TRANSACTION_MINED]: new evt_1.Evt(), [vector_types_1.ChainServiceEvents.TRANSACTION_FAILED]: new evt_1.Evt() });
        Object.entries(chainProviders).forEach(([chainId, provider]) => {
            this.signers.set(parseInt(chainId), typeof signer === "string" ? new wallet_1.Wallet(signer, provider) : signer.connect(provider));
        });
    }
    getSigner(chainId) {
        const signer = this.signers.get(chainId);
        return signer;
    }
    async handleTxSubmit(onchainTransactionId, method, methodId, channelAddress, reason, response) {
        this.log.info({
            method,
            methodId,
            channelAddress,
            reason,
            hash: response.hash,
            gasPrice: response.gasPrice.toString(),
            nonce: response.nonce,
        }, "Tx submitted.");
        await this.store.saveTransactionAttempt(onchainTransactionId, channelAddress, reason, response);
        this.evts[vector_types_1.ChainServiceEvents.TRANSACTION_SUBMITTED].post({
            response: Object.fromEntries(Object.entries(response).map(([key, value]) => {
                return [key, bignumber_1.BigNumber.isBigNumber(value) ? value.toString() : value];
            })),
            channelAddress,
            reason,
        });
    }
    async handleTxMined(onchainTransactionId, method, methodId, channelAddress, reason, receipt) {
        this.log.info({ method, methodId, onchainTransactionId, reason, receipt }, "Tx mined.");
        await this.store.saveTransactionReceipt(onchainTransactionId, receipt);
        this.evts[vector_types_1.ChainServiceEvents.TRANSACTION_MINED].post({
            receipt: Object.fromEntries(Object.entries(receipt).map(([key, value]) => {
                return [key, bignumber_1.BigNumber.isBigNumber(value) ? value.toString() : value];
            })),
            channelAddress,
            reason,
        });
    }
    async handleTxFail(onchainTransactionId, method, methodId, channelAddress, reason, receipt, error, message = "Tx reverted") {
        var _a;
        this.log.error({ method, methodId, error: error ? vector_types_1.jsonifyError(error) : message }, message);
        await this.store.saveTransactionFailure(onchainTransactionId, (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : message, receipt);
        this.evts[vector_types_1.ChainServiceEvents.TRANSACTION_FAILED].post(Object.assign({ error,
            channelAddress,
            reason }, (receipt
            ? {
                receipt: Object.fromEntries(Object.entries(receipt).map(([key, value]) => {
                    return [key, bignumber_1.BigNumber.isBigNumber(value) ? value.toString() : value];
                })),
            }
            : {})));
    }
    async sendTx(txFn, gasPrice, signer, nonce) {
        return await this.queue.add(async () => {
            try {
                const actualNonce = nonce !== null && nonce !== void 0 ? nonce : (await signer.getTransactionCount("pending"));
                const response = await txFn(gasPrice, actualNonce);
                return vector_types_1.Result.ok(response);
            }
            catch (e) {
                return vector_types_1.Result.fail(e);
            }
        });
    }
    async revitalizeTxs() {
        const storedTransactions = await this.store.getActiveTransactions();
        for (const tx of storedTransactions) {
            const latestAttempt = tx.attempts[tx.attempts.length - 1];
            const txResponseRes = await this.getTxResponseFromHash(tx.chainId, latestAttempt.transactionHash);
            if (txResponseRes.isError) {
                this.log.error({ error: txResponseRes.getError(), tx }, "Error in getTxResponseFromHash");
                continue;
            }
            const txResponse = txResponseRes.getValue();
            if (txResponse.receipt) {
                this.log.info({ txHash: txResponse.receipt.transactionHash }, "Tx mined, saving to db");
                await this.store.saveTransactionReceipt(tx.channelAddress, txResponse.receipt);
                continue;
            }
            this.sendTxWithRetries(tx.to, tx.chainId, tx.reason, async () => {
                const signer = this.getSigner(tx.chainId);
                if (!signer) {
                    throw new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound, { chainId: tx.chainId });
                }
                return signer.sendTransaction({
                    to: tx.to,
                    data: tx.data,
                    chainId: tx.chainId,
                    gasPrice: latestAttempt.gasPrice,
                    nonce: tx.nonce,
                    value: bignumber_1.BigNumber.from(tx.value || 0),
                });
            });
        }
    }
    async getTxResponseFromHash(chainId, txHash) {
        const provider = this.chainProviders[chainId];
        if (!provider) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.ProviderNotFound, { chainId }));
        }
        try {
            const response = await provider.getTransaction(txHash);
            let receipt;
            if ((response === null || response === void 0 ? void 0 : response.confirmations) > 0) {
                receipt = await provider.send("eth_getTransactionReceipt", [txHash]);
            }
            return vector_types_1.Result.ok({ response, receipt });
        }
        catch (e) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TxNotFound, { error: e, transactionHash: txHash }));
        }
    }
    async sendTxWithRetries(channelAddress, chainId, reason, txFn) {
        var _a;
        const method = "sendTxWithRetries";
        const methodId = vector_utils_1.getRandomBytes32();
        const errors = [];
        for (let attempt = 0; attempt < this.defaultRetries; attempt++) {
            this.log.info({
                method,
                methodId,
                retries: this.defaultRetries,
                attempt,
                channelAddress,
                chainId,
                reason,
            }, "Attempting to send tx");
            const receipt = await this.sendAndConfirmTx(channelAddress, chainId, reason, txFn);
            if (!receipt.isError) {
                this.log.info({
                    method,
                    methodId,
                    transactionHash: (_a = receipt.getValue()) === null || _a === void 0 ? void 0 : _a.transactionHash,
                    channelAddress,
                    chainId,
                }, receipt.getValue() ? "Tx confirmed" : "Tx was not sent");
                return receipt;
            }
            const error = receipt.getError();
            if (!error.canRetry) {
                this.log.error({ error: error.message, channelAddress, reason, stack: error.stack, method, methodId }, "Failed to send tx, will not retry");
                return receipt;
            }
            errors.push(error);
            this.log.warn({ error: error.message, channelAddress, attempt, retries: this.defaultRetries, method, methodId }, "Tx failed, waiting before retry");
            await vector_utils_1.delay(1000);
        }
        return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.FailedToSendTx, {
            errors: errors.map((e) => e.message).toString(),
            retries: this.defaultRetries,
            channelAddress,
            reason,
        }));
    }
    async sendAndConfirmTx(channelAddress, chainId, reason, txFn, presetGasPrice) {
        const method = "sendAndConfirmTx";
        const methodId = vector_utils_1.getRandomBytes32();
        const signer = this.signers.get(chainId);
        if (!signer) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        let tryNumber = 0;
        let responses = [];
        let nonce;
        let receipt;
        let gasPrice;
        try {
            gasPrice =
                presetGasPrice !== null && presetGasPrice !== void 0 ? presetGasPrice : (await (async () => {
                    const price = await this.getGasPrice(chainId);
                    if (price.isError) {
                        throw price.getError();
                    }
                    return price.getValue();
                })());
        }
        catch (e) {
            return vector_types_1.Result.fail(e);
        }
        let onchainTransactionId = uuid_1.v4();
        while (!receipt) {
            tryNumber += 1;
            try {
                this.log.debug({ method, methodId, nonce, tryNumber, channelAddress, gasPrice: gasPrice.toString() }, "Attempting to send transaction");
                const result = await this.sendTx(txFn, gasPrice, signer, nonce);
                if (!result.isError) {
                    const response = result.getValue();
                    if (response) {
                        if (nonce === undefined) {
                            nonce = response.nonce;
                        }
                        else if (response.nonce != nonce) {
                        }
                        this.log.info({
                            hash: response.hash,
                            gas: response.gasPrice.toString(),
                            channelAddress,
                            method,
                            methodId,
                            nonce: response.nonce,
                        }, "Tx submitted");
                        responses.push(response);
                        await this.handleTxSubmit(onchainTransactionId, method, methodId, channelAddress, reason, response);
                    }
                    else {
                        this.log.info({ method, methodId, channelAddress, reason, tryNumber, nonce, gasPrice: gasPrice.toString() }, "Did not attempt tx.");
                        if (responses.length === 0) {
                            this.log.info({ method, methodId, channelAddress, tryNumber, nonce, gasPrice: gasPrice.toString() }, "Tx not needed");
                            return vector_types_1.Result.ok(undefined);
                        }
                        else {
                            this.log.info({ method, methodId, channelAddress, reason, tryNumber, nonce, gasPrice: gasPrice.toString() }, `txFn returned undefined, proceeding to confirmation step.`);
                        }
                    }
                }
                else {
                    const error = result.getError();
                    if (responses.length >= 1 &&
                        (error.message.includes("nonce has already been used") ||
                            error.message.includes("Transaction nonce is too low.") ||
                            error.message.includes("There is another transaction with same nonce in the queue."))) {
                        this.log.info({ method, methodId, channelAddress, reason, nonce, error: error.message }, "Nonce already used: proceeding to check for confirmation in previous transactions.");
                    }
                    else {
                        this.log.error({ method, methodId, channelAddress, reason, nonce, tryNumber, gasPrice, error }, "Error occurred while executing tx submit.");
                        throw error;
                    }
                }
                receipt = await this.waitForConfirmation(chainId, responses);
                if (receipt && receipt.status === 0) {
                    throw new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TxReverted, { receipt });
                }
            }
            catch (e) {
                if (e.message === vector_types_1.ChainError.reasons.ConfirmationTimeout) {
                    const bumpedGasPrice = gasPrice.add(gasPrice.mul(exports.GAS_BUMP_PERCENT).div(100)).add(1);
                    this.log.info({
                        channelAddress,
                        reason,
                        method,
                        methodId,
                        gasPrice: gasPrice.toString(),
                        bumpedGasPrice: bumpedGasPrice.toString(),
                    }, "Tx timed out waiting for confirmation. Bumping gas price and reattempting.");
                    gasPrice = bumpedGasPrice;
                    if (gasPrice.gt(exports.BIG_GAS_PRICE)) {
                        const error = new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MaxGasPriceReached, {
                            gasPrice: gasPrice.toString(),
                            methodId,
                            method,
                            max: exports.BIG_GAS_PRICE,
                        });
                        await this.handleTxFail(onchainTransactionId, method, methodId, channelAddress, reason, receipt, error, "Max gas price reached");
                        return vector_types_1.Result.fail(error);
                    }
                    continue;
                }
                else {
                    let error = e;
                    if (e.message.includes("sender doesn't have enough funds")) {
                        error = new vector_types_1.ChainError(vector_types_1.ChainError.reasons.NotEnoughFunds);
                    }
                    if (responses.length > 0) {
                        await this.handleTxFail(onchainTransactionId, method, methodId, channelAddress, reason, receipt, e, "Tx reverted");
                    }
                    else {
                        this.log.error({ method, methodId, channelAddress, reason, error: vector_types_1.jsonifyError(error) }, "Tx was never sent due to error");
                    }
                    return vector_types_1.Result.fail(error);
                }
            }
        }
        await this.handleTxMined(onchainTransactionId, method, methodId, channelAddress, reason, receipt);
        return vector_types_1.Result.ok(receipt);
    }
    async waitForConfirmation(chainId, responses) {
        const provider = this.chainProviders[chainId];
        if (!provider) {
            throw new vector_types_1.ChainError(vector_types_1.ChainError.reasons.ProviderNotFound);
        }
        const numConfirmations = vector_types_1.getConfirmationsForChain(chainId);
        const pollForReceipt = async () => {
            let reverted = [];
            const receipt = await Promise.race(responses
                .map((response) => {
                return new Promise(async (resolve) => {
                    const r = await provider.getTransactionReceipt(response.hash);
                    if (r) {
                        if (r.status === 0) {
                            reverted.push(r);
                        }
                        else if (r.confirmations >= numConfirmations) {
                            return resolve(r);
                        }
                    }
                });
            })
                .concat(vector_utils_1.delay(2000)));
            if (!!receipt) {
                if (reverted.length === responses.length) {
                    return reverted[0];
                }
            }
            return receipt;
        };
        let receipt = await pollForReceipt();
        let timeElapsed = 0;
        const startMark = new Date().getTime();
        while (!receipt && timeElapsed < exports.CONFIRMATION_TIMEOUT) {
            receipt = await pollForReceipt();
            timeElapsed = new Date().getTime() - startMark;
        }
        if (!receipt) {
            throw new vector_types_1.ChainError(vector_types_1.ChainError.reasons.ConfirmationTimeout);
        }
        return receipt;
    }
    async sendDeployChannelTx(channelState, deposit) {
        const method = "sendDeployChannelTx";
        const methodId = vector_utils_1.getRandomBytes32();
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        const sender = await signer.getAddress();
        const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
        if (multisigRes.isError) {
            return vector_types_1.Result.fail(multisigRes.getError());
        }
        if (multisigRes.getValue() !== `0x`) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MultisigDeployed));
        }
        const channelFactory = new contracts_1.Contract(channelState.networkContext.channelFactoryAddress, artifacts_1.ChannelFactory.abi, signer);
        if (!deposit) {
            this.log.info({ channelAddress: channelState.channelAddress, sender, method, methodId }, "Deploying channel without deposit");
            const result = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.deploy, async (gasPrice, nonce) => {
                const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
                if (multisigRes.isError) {
                    throw multisigRes.getError();
                }
                if (multisigRes.getValue() !== `0x`) {
                    return undefined;
                }
                return channelFactory.createChannel(channelState.alice, channelState.bob, {
                    gasPrice: gasPrice,
                    gasLimit: exports.BIG_GAS_LIMIT,
                    nonce,
                });
            });
            if (result.isError) {
                return result;
            }
            if (!result.getValue()) {
                return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MultisigDeployed));
            }
            return vector_types_1.Result.ok(result.getValue());
        }
        if (sender !== channelState.alice) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.FailedToDeploy, {
                message: "Sender is not alice",
                sender,
                alice: channelState.alice,
                channel: channelState.channelAddress,
            }));
        }
        const { assetId, amount } = deposit;
        const balanceRes = await this.getOnchainBalance(assetId, channelState.alice, channelState.networkContext.chainId);
        if (balanceRes.isError) {
            return vector_types_1.Result.fail(balanceRes.getError());
        }
        const balance = balanceRes.getValue();
        if (balance.lt(amount)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.NotEnoughFunds, {
                balance: balance.toString(),
                amount,
                assetId,
                chainId: channelState.networkContext.chainId,
            }));
        }
        this.log.info({ balance: balance.toString(), method, methodId, assetId, chainId: channelState.networkContext.chainId }, "Onchain balance sufficient");
        if (assetId === constants_1.AddressZero) {
            const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.deployWithDepositAlice, async (gasPrice, nonce) => {
                const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
                if (multisigRes.isError) {
                    throw multisigRes.getError();
                }
                if (multisigRes.getValue() !== `0x`) {
                    return undefined;
                }
                return channelFactory.createChannelAndDepositAlice(channelState.alice, channelState.bob, assetId, amount, {
                    value: amount,
                    gasPrice: gasPrice,
                    gasLimit: exports.BIG_GAS_LIMIT,
                    nonce,
                });
            });
            if (res.isError) {
                return vector_types_1.Result.fail(res.getError());
            }
            if (!res.getValue()) {
                return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MultisigDeployed));
            }
            return vector_types_1.Result.ok(res.getValue());
        }
        this.log.info({ assetId, amount, channel: channelState.channelAddress, sender }, "Approving tokens");
        const approveRes = await this.approveTokens(channelState.channelAddress, channelState.networkContext.channelFactoryAddress, sender, amount, assetId, channelState.networkContext.chainId);
        if (approveRes.isError) {
            return vector_types_1.Result.fail(approveRes.getError());
        }
        if (approveRes.getValue()) {
            const receipt = approveRes.getValue();
            if (receipt.status === 0) {
                return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TxReverted, { receipt }));
            }
            this.log.info({ txHash: receipt.transactionHash, method, assetId }, "Token approval confirmed");
        }
        const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.deployWithDepositAlice, async (gasPrice, nonce) => {
            const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
            if (multisigRes.isError) {
                throw multisigRes.getError();
            }
            if (multisigRes.getValue() !== `0x`) {
                return undefined;
            }
            return channelFactory.createChannelAndDepositAlice(channelState.alice, channelState.bob, assetId, amount, {
                gasPrice: gasPrice,
                gasLimit: exports.BIG_GAS_LIMIT,
                nonce,
            });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        if (!res.getValue()) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MultisigDeployed));
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendWithdrawTx(channelState, minTx) {
        var _a;
        const method = "sendWithdrawTx";
        const methodId = vector_utils_1.getRandomBytes32();
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        const sender = await signer.getAddress();
        if (channelState.alice !== sender && channelState.bob !== sender) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SenderNotInChannel));
        }
        const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
        if (multisigRes.isError) {
            return vector_types_1.Result.fail(multisigRes.getError());
        }
        if (multisigRes.getValue() === `0x`) {
            this.log.info({ channelAddress: channelState.channelAddress, sender, method, methodId }, "Deploying channel");
            const txRes = await this.sendDeployChannelTx(channelState);
            if (txRes.isError && ((_a = txRes.getError()) === null || _a === void 0 ? void 0 : _a.message) !== vector_types_1.ChainError.reasons.MultisigDeployed) {
                return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.FailedToDeploy, {
                    method,
                    error: txRes.getError().message,
                    channel: channelState.channelAddress,
                }));
            }
        }
        this.log.info({ sender, method, methodId, channel: channelState.channelAddress }, "Sending withdraw tx to chain");
        const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.withdraw, async (gasPrice, nonce) => {
            return signer.sendTransaction(Object.assign(Object.assign({}, minTx), { gasPrice, gasLimit: exports.BIG_GAS_LIMIT, from: sender, nonce }));
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendDepositTx(channelState, sender, amount, assetId) {
        const method = "sendDepositTx";
        const methodId = vector_utils_1.getRandomBytes32();
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        if (channelState.alice !== sender && channelState.bob !== sender) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SenderNotInChannel));
        }
        const toDeposit = bignumber_1.BigNumber.from(amount);
        if (toDeposit.isNegative()) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.NegativeDepositAmount));
        }
        const multisigRes = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
        if (multisigRes.isError) {
            return vector_types_1.Result.fail(multisigRes.getError());
        }
        const multisigCode = multisigRes.getValue();
        if (multisigCode === `0x` && sender === channelState.alice) {
            this.log.info({
                method,
                methodId,
                channelAddress: channelState.channelAddress,
                assetId,
                amount,
                senderAddress: await signer.getAddress(),
            }, `Deploying channel with deposit`);
            const deployResult = await this.sendDeployChannelTx(channelState, { amount, assetId });
            if (deployResult.isError) {
                const error = deployResult.getError();
                if (!(error.message === vector_types_1.ChainError.reasons.MultisigDeployed)) {
                    return vector_types_1.Result.fail(deployResult.getError());
                }
            }
            else {
                return vector_types_1.Result.ok(deployResult.getValue());
            }
        }
        const balanceRes = await this.getOnchainBalance(assetId, sender, channelState.networkContext.chainId);
        if (balanceRes.isError) {
            return vector_types_1.Result.fail(balanceRes.getError());
        }
        const balance = balanceRes.getValue();
        if (balance.lt(amount)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.NotEnoughFunds, {
                balance: balance.toString(),
                amount,
                assetId,
                chainId: channelState.networkContext.chainId,
            }));
        }
        this.log.info({ balance: balance.toString(), method, methodId, assetId, chainId: channelState.networkContext.chainId }, "Onchain balance sufficient");
        this.log.info({ method, methodId, assetId, amount }, "Channel is deployed, sending deposit");
        if (sender === channelState.alice) {
            this.log.info({ method, sender, alice: channelState.alice, bob: channelState.bob }, "Detected participant A, sending tx");
            const txRes = await this.sendDepositATx(channelState, amount, assetId);
            if (txRes.isError) {
                this.log.error({ method, error: txRes.getError().message }, "Error sending tx");
            }
            else {
                this.log.info({ method, txHash: txRes.getValue().transactionHash }, "Tx mined");
            }
            return txRes;
        }
        else {
            this.log.info({ method, sender, alice: channelState.alice, bob: channelState.bob }, "Detected participant B, sendng tx");
            const txRes = await this.sendDepositBTx(channelState, amount, assetId);
            if (txRes.isError) {
                this.log.error({ method, error: txRes.getError().message }, "Error sending tx");
            }
            else {
                this.log.info({ method, txHash: txRes.getValue().transactionHash }, "Tx mined");
            }
            return txRes;
        }
    }
    on(event, callback, filter = () => true) {
        this.evts[event].pipe(filter).attach(callback);
    }
    once(event, callback, filter = () => true) {
        this.evts[event].pipe(filter).attachOnce(callback);
    }
    off(event) {
        if (event) {
            this.evts[event].detach();
            return;
        }
        Object.values(this.evts).forEach((evt) => evt.detach());
    }
    waitFor(event, timeout, filter = () => true) {
        return this.evts[event].pipe(filter).waitFor(timeout);
    }
    async approveTokens(channelAddress, spender, owner, depositAmount, assetId, chainId, approvalAmount = vector_types_1.UINT_MAX) {
        var _a, _b;
        const method = "approveTokens";
        this.log.debug({
            method,
            channelAddress,
            spender,
            owner,
            approvalAmount,
            depositAmount,
            assetId,
            chainId,
        }, "Method started");
        const signer = this.signers.get(chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        this.log.info({ method, assetId, spender, owner, channelAddress }, "Checking allowance");
        const erc20 = new contracts_1.Contract(assetId, vector_types_1.ERC20Abi, signer);
        const allowanceRes = await this.getTokenAllowance(assetId, owner, spender, chainId);
        if (allowanceRes.isError) {
            this.log.error({
                method,
                spender,
                owner,
                assetId,
                error: (_a = allowanceRes.getError()) === null || _a === void 0 ? void 0 : _a.message,
            }, "Error checking approved tokens for deposit A");
            return vector_types_1.Result.fail(allowanceRes.getError());
        }
        const allowance = allowanceRes.getValue();
        this.log.info({ method, assetId, spender, owner, channelAddress, allowance: allowance.toString(), depositAmount }, "Retrieved allowance");
        if (bignumber_1.BigNumber.from(allowanceRes.getValue()).gte(depositAmount)) {
            this.log.info({
                method,
                assetId,
                channelAddress,
            }, "Allowance is sufficient");
            return vector_types_1.Result.ok(undefined);
        }
        this.log.info({
            method,
            assetId,
            channelAddress,
            spender,
            owner,
            approvalAmount,
        }, "Approving tokens");
        const approveRes = await this.sendTxWithRetries(channelAddress, chainId, vector_types_1.TransactionReason.approveTokens, async (gasPrice, nonce) => {
            return erc20.approve(spender, approvalAmount, { gasPrice, nonce });
        });
        if (approveRes.isError) {
            this.log.error({
                method,
                spender,
                owner,
                assetId,
                approvalAmount,
                allowance: allowance.toString(),
                error: (_b = approveRes.getError()) === null || _b === void 0 ? void 0 : _b.message,
            }, "Error approving tokens for deposit A");
            return approveRes;
        }
        const approveTx = approveRes.getValue();
        this.log.info({ txHash: approveTx.transactionHash, method, assetId, approvalAmount }, "Approve token tx mined");
        return approveRes;
    }
    async sendDepositATx(channelState, amount, assetId) {
        var _a, _b;
        const method = "sendDepositATx";
        const methodId = vector_utils_1.getRandomBytes32();
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        const vectorChannel = new contracts_1.Contract(channelState.channelAddress, artifacts_1.VectorChannel.abi, signer);
        if (assetId !== constants_1.AddressZero) {
            this.log.info({ method, methodId, assetId, channelAddress: channelState.channelAddress }, "Approving token");
            const approveRes = await this.approveTokens(channelState.channelAddress, channelState.channelAddress, channelState.alice, amount, assetId, channelState.networkContext.chainId);
            if (approveRes.isError) {
                this.log.error({
                    method,
                    methodId,
                    channelAddress: channelState.channelAddress,
                    error: (_a = approveRes.getError()) === null || _a === void 0 ? void 0 : _a.message,
                }, "Error approving tokens for deposit A");
                return vector_types_1.Result.fail(approveRes.getError());
            }
            this.log.info({ txHash: (_b = approveRes.getValue()) === null || _b === void 0 ? void 0 : _b.transactionHash, method, methodId, assetId }, "Token approval confirmed");
            const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositA, async (gasPrice, nonce) => {
                return vectorChannel.depositAlice(assetId, amount, { gasPrice, nonce });
            });
            if (res.isError) {
                return vector_types_1.Result.fail(res.getError());
            }
            return vector_types_1.Result.ok(res.getValue());
        }
        const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositA, async (gasPrice, nonce) => {
            return vectorChannel.depositAlice(assetId, amount, { value: amount, gasPrice, nonce });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendDepositBTx(channelState, amount, assetId) {
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        const sender = await signer.getAddress();
        if (assetId === constants_1.AddressZero) {
            const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositB, async (gasPrice, nonce) => {
                return signer.sendTransaction({
                    data: "0x",
                    to: channelState.channelAddress,
                    value: bignumber_1.BigNumber.from(amount),
                    chainId: channelState.networkContext.chainId,
                    gasPrice,
                    from: sender,
                    nonce,
                });
            });
            if (res.isError) {
                return vector_types_1.Result.fail(res.getError());
            }
            return vector_types_1.Result.ok(res.getValue());
        }
        else {
            const erc20 = new contracts_1.Contract(channelState.networkContext.channelFactoryAddress, vector_types_1.ERC20Abi, signer);
            const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositB, async (gasPrice, nonce) => {
                return erc20.transfer(channelState.channelAddress, amount, { gasPrice, nonce });
            });
            if (res.isError) {
                return vector_types_1.Result.fail(res.getError());
            }
            return vector_types_1.Result.ok(res.getValue());
        }
    }
    async sendDisputeChannelTx(channelState) {
        const method = "sendDisputeChannelTx";
        const methodId = vector_utils_1.getRandomBytes32();
        this.log.info({ method, methodId, channelAddress: channelState.channelAddress }, "Method started");
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MissingSigs));
        }
        const code = await this.getCode(channelState.channelAddress, channelState.networkContext.chainId);
        if (code.isError) {
            return vector_types_1.Result.fail(code.getError());
        }
        if (code.getValue() === "0x") {
            this.log.info({ method, methodId, channelAddress: channelState.channelAddress, chainId: channelState.networkContext.chainId }, "Deploying channel");
            const gasPrice = await this.getGasPrice(channelState.networkContext.chainId);
            if (gasPrice.isError) {
                return vector_types_1.Result.fail(gasPrice.getError());
            }
            const deploy = await this.sendDeployChannelTx(channelState);
            if (deploy.isError) {
                return vector_types_1.Result.fail(deploy.getError());
            }
            this.log.info({
                method,
                methodId,
                channelAddress: channelState.channelAddress,
                transactionHash: deploy.getValue().transactionHash,
            }, "Channel deployed");
        }
        const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.disputeChannel, (gasPrice, nonce) => {
            const channel = new contracts_1.Contract(channelState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.disputeChannel(channelState, channelState.latestUpdate.aliceSignature, channelState.latestUpdate.bobSignature, {
                gasPrice,
                nonce,
            });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendDefundChannelTx(channelState, assetsToDefund = channelState.assetIds, indices = []) {
        const signer = this.signers.get(channelState.networkContext.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        if (!channelState.latestUpdate.aliceSignature || !channelState.latestUpdate.bobSignature) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.MissingSigs));
        }
        const res = await this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.defundChannel, (gasPrice, nonce) => {
            const channel = new contracts_1.Contract(channelState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.defundChannel(channelState, assetsToDefund, indices.length > 0 ? indices : assetsToDefund.map((_asset, idx) => idx), { gasPrice, nonce });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendDisputeTransferTx(transferIdToDispute, activeTransfers) {
        const transferState = activeTransfers.find((t) => t.transferId === transferIdToDispute);
        if (!transferState) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TransferNotFound, {
                transfer: transferIdToDispute,
                active: activeTransfers.map((t) => t.transferId),
            }));
        }
        const signer = this.signers.get(transferState.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        const { tree } = vector_utils_1.generateMerkleTreeData(activeTransfers);
        const res = await this.sendTxWithRetries(transferState.channelAddress, transferState.chainId, vector_types_1.TransactionReason.disputeTransfer, (gasPrice, nonce) => {
            const channel = new contracts_1.Contract(transferState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.disputeTransfer(transferState, tree.getHexProof(vector_utils_1.hashCoreTransferState(transferState)), {
                gasPrice,
                nonce,
            });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendDefundTransferTx(transferState, responderSignature = constants_1.HashZero) {
        const signer = this.signers.get(transferState.chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        if (!transferState.transferResolver) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.ResolverNeeded));
        }
        const encodedState = vector_utils_1.encodeTransferState(transferState.transferState, transferState.transferEncodings[0]);
        const encodedResolver = vector_utils_1.encodeTransferResolver(transferState.transferResolver, transferState.transferEncodings[1]);
        const res = await this.sendTxWithRetries(transferState.channelAddress, transferState.chainId, vector_types_1.TransactionReason.defundTransfer, (gasPrice, nonce) => {
            const channel = new contracts_1.Contract(transferState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.defundTransfer(transferState, encodedState, encodedResolver, responderSignature, {
                gasPrice,
                nonce,
            });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
    async sendExitChannelTx(channelAddress, chainId, assetId, owner, recipient) {
        const signer = this.signers.get(chainId);
        if (!(signer === null || signer === void 0 ? void 0 : signer._isSigner)) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.SignerNotFound));
        }
        this.log.info({ channelAddress, chainId, assetId, owner, recipient }, "Defunding channel");
        const res = await this.sendTxWithRetries(channelAddress, chainId, vector_types_1.TransactionReason.exitChannel, (gasPrice, nonce) => {
            const channel = new contracts_1.Contract(channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.exit(assetId, owner, recipient, { gasPrice, nonce });
        });
        if (res.isError) {
            return vector_types_1.Result.fail(res.getError());
        }
        return vector_types_1.Result.ok(res.getValue());
    }
}
exports.EthereumChainService = EthereumChainService;
//# sourceMappingURL=ethService.js.map