"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_LIMIT = exports.EXTRA_GAS = 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 keccak256_1 = require("@ethersproject/keccak256");
const wallet_1 = require("@ethersproject/wallet");
const p_queue_1 = __importDefault(require("p-queue"));
const constants_1 = require("@ethersproject/constants");
const merkletreejs_1 = require("merkletreejs");
const evt_1 = require("evt");
const artifacts_1 = require("../artifacts");
const ethReader_1 = require("./ethReader");
const utils_1 = require("ethers/lib/utils");
exports.EXTRA_GAS = 50000;
exports.BIG_GAS_LIMIT = bignumber_1.BigNumber.from(1000000);
const waitForTransaction = async (provider, transactionHash, confirmations, timeout) => {
    try {
        const receipt = await provider.waitForTransaction(transactionHash, confirmations, timeout);
        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 = 1) {
        super(chainProviders, log.child({ module: "EthereumChainService" }));
        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));
        });
    }
    async speedUpTx(chainId, tx) {
        const method = "speedUpTx";
        const methodId = vector_utils_1.getRandomBytes32();
        this.log.info({ method, methodId, transactionHash: tx.transactionHash }, "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));
        }
        let receipt;
        try {
            receipt = await signer.provider.getTransaction(tx.transactionHash);
        }
        catch (e) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError("Could not get transaction", { error: e.message, transactionHash: tx.transactionHash }));
        }
        if (receipt && receipt.confirmations > 0) {
            return vector_types_1.Result.fail(new vector_types_1.ChainError("Transaction mined", {
                transactionHash: tx.transactionHash,
                confirmations: receipt.confirmations,
                blockNumber: receipt.blockNumber,
            }));
        }
        return this.sendTxWithRetries(tx.to, chainId, vector_types_1.TransactionReason.speedUpTransaction, async () => {
            var _a, _b;
            const price = await this.getGasPrice(chainId);
            if (price.isError) {
                throw price.getError();
            }
            const current = price.getValue().add(utils_1.parseUnits("20", "gwei"));
            const increased = current.gt((_a = receipt === null || receipt === void 0 ? void 0 : receipt.gasPrice) !== null && _a !== void 0 ? _a : 0)
                ? current
                : bignumber_1.BigNumber.from((_b = receipt === null || receipt === void 0 ? void 0 : receipt.gasPrice) !== null && _b !== void 0 ? _b : 0).add(utils_1.parseUnits("20", "gwei"));
            return signer.sendTransaction({
                to: tx.to,
                data: tx.data,
                chainId,
                gasPrice: increased,
                nonce: tx.nonce,
                value: bignumber_1.BigNumber.from(tx.value),
            });
        });
    }
    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 deploy = await this.sendDeployChannelTx(channelState);
            if (deploy.isError) {
                return vector_types_1.Result.fail(deploy.getError());
            }
            this.log.debug({ method, methodId, channelAddress: channelState.channelAddress, transactionHash: deploy.getValue().hash }, "Deploy channel tx");
            const result = await deploy.getValue().completed();
            if (result.isError) {
                return vector_types_1.Result.fail(result.getError());
            }
            this.log.info({
                method,
                methodId,
                channelAddress: channelState.channelAddress,
                transactionHash: result.getValue().transactionHash,
            }, "Channel deployed");
        }
        return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.disputeChannel, () => {
            const channel = new contracts_1.Contract(channelState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.disputeChannel(channelState, channelState.latestUpdate.aliceSignature, channelState.latestUpdate.bobSignature);
        });
    }
    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));
        }
        return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.defundChannel, () => {
            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));
        });
    }
    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 hashes = activeTransfers.map((t) => vector_utils_1.bufferify(vector_utils_1.hashCoreTransferState(t)));
        const hash = vector_utils_1.bufferify(vector_utils_1.hashCoreTransferState(transferState));
        const merkle = new merkletreejs_1.MerkleTree(hashes, keccak256_1.keccak256);
        return this.sendTxWithRetries(transferState.channelAddress, transferState.chainId, vector_types_1.TransactionReason.disputeTransfer, () => {
            const channel = new contracts_1.Contract(transferState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.disputeTransfer(transferState, merkle.getHexProof(hash));
        });
    }
    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]);
        return this.sendTxWithRetries(transferState.channelAddress, transferState.chainId, vector_types_1.TransactionReason.defundTransfer, () => {
            const channel = new contracts_1.Contract(transferState.channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.defundTransfer(transferState, encodedState, encodedResolver, responderSignature);
        });
    }
    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");
        return this.sendTxWithRetries(channelAddress, chainId, vector_types_1.TransactionReason.exitChannel, () => {
            const channel = new contracts_1.Contract(channelAddress, artifacts_1.VectorChannel.abi, signer);
            return channel.exit(assetId, owner, recipient);
        });
    }
    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 () => {
                const gasPrice = await this.getGasPrice(channelState.networkContext.chainId);
                if (gasPrice.isError) {
                    vector_types_1.Result.fail(gasPrice.getError());
                }
                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 undefined;
                }
                return channelFactory.createChannel(channelState.alice, channelState.bob, {
                    gasPrice: gasPrice.getValue(),
                    gasLimit: exports.BIG_GAS_LIMIT,
                });
            });
            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 result;
        }
        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) {
            return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.deployWithDepositAlice, async () => {
                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 this.sendDepositATx(channelState, amount, constants_1.AddressZero);
                }
                const gasPrice = await this.getGasPrice(channelState.networkContext.chainId);
                if (gasPrice.isError) {
                    vector_types_1.Result.fail(gasPrice.getError());
                }
                return channelFactory.createChannelAndDepositAlice(channelState.alice, channelState.bob, assetId, amount, {
                    value: amount,
                    gasPrice: gasPrice.getValue(),
                    gasLimit: exports.BIG_GAS_LIMIT,
                });
            });
        }
        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 = await approveRes.getValue().wait(vector_types_1.getConfirmationsForChain(channelState.networkContext.chainId));
            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");
        }
        return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.deployWithDepositAlice, async () => {
            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 this.sendDepositATx(channelState, amount, assetId);
            }
            const gasPrice = await this.getGasPrice(channelState.networkContext.chainId);
            if (gasPrice.isError) {
                vector_types_1.Result.fail(gasPrice.getError());
            }
            return channelFactory.createChannelAndDepositAlice(channelState.alice, channelState.bob, assetId, amount, {
                gasPrice: gasPrice.getValue(),
                gasLimit: exports.BIG_GAS_LIMIT,
            });
        });
    }
    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,
                }));
            }
            const deployTx = txRes.isError ? undefined : txRes.getValue();
            if (deployTx) {
                this.log.info({ method, methodId, deployTx: deployTx.hash }, "Deploy tx broadcast");
                try {
                    this.log.debug({
                        method,
                        methodId,
                    }, "Waiting for event to be emitted");
                    const receipt = await deployTx.wait(vector_types_1.getConfirmationsForChain(channelState.networkContext.chainId));
                    if (receipt.status === 0) {
                        return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.TxReverted, {
                            receipt,
                            deployTx: deployTx.hash,
                            channel: channelState.channelAddress,
                            chainId: channelState.networkContext.chainId,
                        }));
                    }
                }
                catch (e) {
                    this.log.error({ method, methodId, error: vector_types_1.jsonifyError(e) }, "Caught error waiting for tx");
                    return vector_types_1.Result.fail(new vector_types_1.ChainError(vector_types_1.ChainError.reasons.FailedToDeploy, {
                        error: e.message,
                        deployTx: deployTx.hash,
                        channel: channelState.channelAddress,
                        chainId: channelState.networkContext.chainId,
                    }));
                }
                this.log.debug({ method, methodId }, "Deploy tx mined");
            }
            else {
                this.log.info({ method, methodId }, "Multisig already deployed");
            }
        }
        this.log.info({ sender, method, methodId, channel: channelState.channelAddress }, "Sending withdraw tx to chain");
        return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.withdraw, async () => {
            const gasPriceRes = await this.getGasPrice(channelState.networkContext.chainId);
            if (gasPriceRes.isError) {
                throw gasPriceRes.getError();
            }
            const gasPrice = gasPriceRes.getValue();
            return signer.sendTransaction(Object.assign(Object.assign({}, minTx), { gasPrice, gasLimit: exports.BIG_GAS_LIMIT, from: sender }));
        });
    }
    async sendDepositTx(channelState, sender, amount, assetId) {
        var _a, _b;
        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`);
            return this.sendDeployChannelTx(channelState, { amount, assetId });
        }
        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: (_a = txRes.getError()) === null || _a === void 0 ? void 0 : _a.message }, "Error sending tx");
            }
            else {
                this.log.info({ method, txHash: txRes.getValue().hash }, "Submitted tx");
            }
            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: (_b = txRes.getError()) === null || _b === void 0 ? void 0 : _b.message }, "Error sending tx");
            }
            else {
                this.log.info({ method, txHash: txRes.getValue().hash }, "Submitted tx");
            }
            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 sendTxWithRetries(channelAddress, chainId, reason, txFn) {
        const method = "sendTxWithRetries";
        const methodId = vector_utils_1.getRandomBytes32();
        const errors = [];
        for (let attempt = 1; attempt++; attempt < this.defaultRetries) {
            this.log.info({
                method,
                methodId,
                retries: this.defaultRetries,
                attempt,
                channelAddress,
                reason,
            }, "Attempting to send tx");
            const response = await this.sendTxAndParseResponse(channelAddress, chainId, reason, txFn);
            if (!response.isError) {
                return response;
            }
            const error = response.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 response;
            }
            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 sendTxAndParseResponse(channelAddress, chainId, reason, txFn) {
        try {
            const response = await this.queue.add(async () => {
                const response = await txFn();
                if (!response) {
                    this.log.warn({ channelAddress, reason }, "Did not attempt tx");
                    return response;
                }
                await this.store.saveTransactionResponse(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,
                });
                response
                    .wait(vector_types_1.getConfirmationsForChain(chainId))
                    .then(async (receipt) => {
                    if (receipt.status === 0) {
                        this.log.error({ method: "sendTxAndParseResponse", receipt }, "Transaction reverted");
                        await this.store.saveTransactionFailure(channelAddress, response.hash, "Tx reverted");
                        this.evts[vector_types_1.ChainServiceEvents.TRANSACTION_FAILED].post({
                            receipt: Object.fromEntries(Object.entries(receipt).map(([key, value]) => {
                                return [key, bignumber_1.BigNumber.isBigNumber(value) ? value.toString() : value];
                            })),
                            channelAddress,
                            reason,
                        });
                    }
                    else {
                        await this.store.saveTransactionReceipt(channelAddress, 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,
                        });
                    }
                })
                    .catch(async (e) => {
                    this.log.error({ method: "sendTxAndParseResponse", error: vector_types_1.jsonifyError(e) }, "Transaction reverted");
                    await this.store.saveTransactionFailure(channelAddress, response.hash, e.message);
                    this.evts[vector_types_1.ChainServiceEvents.TRANSACTION_FAILED].post({
                        error: e,
                        channelAddress,
                        reason,
                    });
                });
                return response;
            });
            if (!response) {
                return vector_types_1.Result.ok(response);
            }
            return vector_types_1.Result.ok(Object.assign(Object.assign({}, response), { completed: async (confirmations) => {
                    try {
                        const receipt = await response.wait(confirmations);
                        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(new vector_types_1.ChainError(e.message, { stack: e.stack, channelAddress, reason }));
                    }
                } }));
        }
        catch (e) {
            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);
            }
            return vector_types_1.Result.fail(error);
        }
    }
    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 () => {
            const gasPriceRes = await this.getGasPrice(chainId);
            if (gasPriceRes.isError) {
                throw gasPriceRes.getError();
            }
            const gasPrice = gasPriceRes.getValue();
            return erc20.approve(spender, approvalAmount, { gasPrice });
        });
        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.hash, method, assetId, approvalAmount }, "Approve token tx submitted");
        return approveRes;
    }
    async sendDepositATx(channelState, amount, assetId) {
        var _a;
        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());
            }
            const approveTx = approveRes.getValue();
            if (approveTx) {
                const receipt = await approveTx.wait();
                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: approveTx === null || approveTx === void 0 ? void 0 : approveTx.hash, method, methodId, assetId }, "Token approval confirmed");
            return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositA, async () => {
                const gasPriceRes = await this.getGasPrice(channelState.networkContext.chainId);
                if (gasPriceRes.isError) {
                    throw gasPriceRes.getError();
                }
                const gasPrice = gasPriceRes.getValue();
                return vectorChannel.depositAlice(assetId, amount, { gasPrice });
            });
        }
        return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositA, async () => {
            const gasPriceRes = await this.getGasPrice(channelState.networkContext.chainId);
            if (gasPriceRes.isError) {
                throw gasPriceRes.getError();
            }
            const gasPrice = gasPriceRes.getValue();
            return vectorChannel.depositAlice(assetId, amount, { value: amount, gasPrice });
        });
    }
    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) {
            return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositB, async () => {
                const gasPriceRes = await this.getGasPrice(channelState.networkContext.chainId);
                if (gasPriceRes.isError) {
                    throw gasPriceRes.getError();
                }
                const gasPrice = gasPriceRes.getValue();
                return signer.sendTransaction({
                    data: "0x",
                    to: channelState.channelAddress,
                    value: bignumber_1.BigNumber.from(amount),
                    chainId: channelState.networkContext.chainId,
                    gasPrice,
                    from: sender,
                });
            });
        }
        else {
            const erc20 = new contracts_1.Contract(channelState.networkContext.channelFactoryAddress, vector_types_1.ERC20Abi, signer);
            return this.sendTxWithRetries(channelState.channelAddress, channelState.networkContext.chainId, vector_types_1.TransactionReason.depositB, async () => {
                const gasPriceRes = await this.getGasPrice(channelState.networkContext.chainId);
                if (gasPriceRes.isError) {
                    throw gasPriceRes.getError();
                }
                const gasPrice = gasPriceRes.getValue();
                return erc20.transfer(channelState.channelAddress, amount, { gasPrice });
            });
        }
    }
}
exports.EthereumChainService = EthereumChainService;
//# sourceMappingURL=ethService.js.map