"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseEVMStateProvider = void 0;
const crypto_rpc_1 = require("@bitpay-labs/crypto-rpc");
const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
const decorators_1 = require("../../../../decorators/decorators");
const logger_1 = __importDefault(require("../../../../logger"));
const cache_1 = require("../../../../models/cache");
const walletAddress_1 = require("../../../../models/walletAddress");
const internal_1 = require("../../../../providers/chain-state/internal/internal");
const config_1 = require("../../../../services/config");
const storage_1 = require("../../../../services/storage");
const utils_1 = require("../../../../utils");
const stats_1 = require("../../../../utils/stats");
const streamWithEventPipe_1 = require("../../../../utils/streamWithEventPipe");
const apiStream_1 = require("../../external/streams/apiStream");
const aavePool_1 = require("../abi/aavePool");
const aavePoolV2_1 = require("../abi/aavePoolV2");
const erc20_1 = require("../abi/erc20");
const multisend_1 = require("../abi/multisend");
const block_1 = require("../models/block");
const transaction_1 = require("../models/transaction");
const aave_1 = require("./aave");
const erc20Transform_1 = require("./erc20Transform");
const internalTxTransform_1 = require("./internalTxTransform");
const populateEffectsTransform_1 = require("./populateEffectsTransform");
const populateReceiptTransform_1 = require("./populateReceiptTransform");
const transform_1 = require("./transform");
;
class BaseEVMStateProvider extends internal_1.InternalStateProvider {
    constructor(chain = 'ETH') {
        super(chain);
        this.chain = chain;
        this.config = config_1.Config.get().chains[this.chain];
        BaseEVMStateProvider.initializeRpcs(this.chain);
    }
    static initializeRpcs(chain) {
        if (BaseEVMStateProvider.rpcInitialized[chain]) {
            return;
        }
        BaseEVMStateProvider.rpcInitialized[chain] = true;
        const configs = config_1.Config.get().chains[chain];
        for (const [network, config] of Object.entries(configs)) {
            const chainNetwork = (0, utils_1.normalizeChainNetwork)(chain, network);
            BaseEVMStateProvider.rpcs[chainNetwork] = { historical: [], realtime: [] };
            BaseEVMStateProvider.rpcIndicies[chainNetwork] = { historical: 0, realtime: 0 };
            const providers = config.provider ? [config.provider] : config.providers || [];
            for (const providerConfig of providers) {
                const rpcConfig = { ...providerConfig, chain, isEVM: true };
                const rpc = new crypto_rpc_1.CryptoRpc(rpcConfig).get(chain);
                const rpcObj = {
                    rpc,
                    web3: rpc.web3,
                    dataType: rpcConfig.dataType || 'combined'
                };
                if (rpcObj.dataType === 'historical' || rpcObj.dataType === 'combined') {
                    BaseEVMStateProvider.rpcs[chainNetwork].historical.push(rpcObj);
                }
                if (rpcObj.dataType === 'realtime' || rpcObj.dataType === 'combined') {
                    BaseEVMStateProvider.rpcs[chainNetwork].realtime.push(rpcObj);
                }
            }
        }
    }
    static teardownRpcs() {
        for (const [chainNetwork, rpcObj] of Object.entries(BaseEVMStateProvider.rpcs)) {
            logger_1.default.info('Tearing down RPC connections for %o', chainNetwork);
            for (const rpc of (rpcObj.historical || []).concat(rpcObj.realtime || [])) {
                try {
                    rpc.web3.currentProvider?.disconnect?.();
                }
                catch { /* ignore -- already disconnected or non-socket connection (e.g. http) */ }
            }
            delete BaseEVMStateProvider.rpcs[chainNetwork];
            delete BaseEVMStateProvider.rpcIndicies[chainNetwork];
            delete BaseEVMStateProvider.rpcInitialized[chainNetwork.split(':')[0]];
        }
    }
    async getWeb3(network, params) {
        const chainNetwork = (0, utils_1.normalizeChainNetwork)(this.chain, network);
        const type = params?.type || 'realtime';
        if (!BaseEVMStateProvider.rpcs[chainNetwork]?.[type]?.length) {
            throw new Error(`No configuration found for ${chainNetwork} and "${type}" compatible dataType`);
        }
        if (BaseEVMStateProvider.rpcs[chainNetwork][type].length === 1) {
            return BaseEVMStateProvider.rpcs[chainNetwork][type][0];
        }
        // Load-balance the RPCs in a round-robin fashion
        const lastUsedIndex = BaseEVMStateProvider.rpcIndicies[chainNetwork][type];
        const getNextIndex = (index) => (index + 1) % BaseEVMStateProvider.rpcs[chainNetwork][type].length;
        const initialIndex = getNextIndex(lastUsedIndex);
        let index = initialIndex;
        let rpc;
        do {
            rpc = BaseEVMStateProvider.rpcs[chainNetwork][type][index];
            try {
                if (Date.now() - (rpc.lastPingTime || 0) < 10000) { // Keep the rpc from being blasted with ping calls
                    return rpc;
                }
                await Promise.race([
                    rpc.web3.eth.getBlockNumber(),
                    new Promise((_, reject) => setTimeout(reject, 5000))
                ]);
                rpc.lastPingTime = Date.now();
                // Update the most recently used index
                BaseEVMStateProvider.rpcIndicies[chainNetwork][type] = index;
                return rpc;
            }
            catch {
                if (rpc.web3.currentProvider instanceof crypto_wallet_core_1.Web3.providers.WebsocketProvider &&
                    (['connected', 'connecting'].includes(rpc.web3.currentProvider.getStatus()))) {
                    try {
                        // try reconnecting
                        rpc.web3.currentProvider.disconnect();
                        rpc.web3.currentProvider.connect();
                    }
                    catch (err) {
                        logger_1.default.warn('Error reconnecting to %o:%o RPC websocket: %o', this.chain, network, err);
                    }
                }
            }
            index = getNextIndex(index);
        } while (index !== initialIndex);
        // If none have worked, return the last (successful?) rpc
        logger_1.default.warn('All %o:%o RPCs are unresponsive, returning last used RPC', this.chain, network);
        return BaseEVMStateProvider.rpcs[chainNetwork][type][lastUsedIndex];
    }
    async erc20For(network, address) {
        const { web3 } = await this.getWeb3(network);
        const contract = new web3.eth.Contract(erc20_1.ERC20Abi, address);
        return contract;
    }
    async getMultisendContract(network, address) {
        const { web3 } = await this.getWeb3(network);
        const contract = new web3.eth.Contract(multisend_1.MultisendAbi, address);
        return contract;
    }
    async getERC20TokenInfo(network, tokenAddress) {
        const token = await this.erc20For(network, tokenAddress);
        const [name, decimals, symbol] = await Promise.all([
            token.methods.name().call(),
            token.methods.decimals().call(),
            token.methods.symbol().call()
        ]);
        return {
            name,
            decimals: Number(decimals),
            symbol
        };
    }
    async getERC20TokenAllowance(network, tokenAddress, ownerAddress, spenderAddress) {
        const token = await this.erc20For(network, tokenAddress);
        return Number(await token.methods.allowance(ownerAddress, spenderAddress).call());
    }
    async getAaveUserAccountData(params) {
        const { network, address, version } = params;
        const poolAddress = (0, aave_1.getAavePoolAddress)(this.chain, network, version);
        if (!poolAddress) {
            throw new Error(`Unsupported Aave pool for chain "${this.chain}", network "${network}", version "${version}".`);
        }
        const { web3 } = await this.getWeb3(network);
        if (version === 'v2') {
            return this.getAaveV2UserAccountData(web3, poolAddress, address);
        }
        return this.getAaveV3UserAccountData(web3, poolAddress, address);
    }
    async getAaveV2UserAccountData(web3, poolAddress, address) {
        const contract = new web3.eth.Contract(aavePoolV2_1.AavePoolAbiV2, poolAddress);
        const accountData = await contract.methods
            .getUserAccountData(web3.utils.toChecksumAddress(address))
            .call();
        return {
            totalCollateralETH: accountData.totalCollateralETH.toString(),
            totalDebtETH: accountData.totalDebtETH.toString(),
            availableBorrowsETH: accountData.availableBorrowsETH.toString(),
            currentLiquidationThreshold: accountData.currentLiquidationThreshold.toString(),
            ltv: accountData.ltv.toString(),
            healthFactor: accountData.healthFactor.toString()
        };
    }
    async getAaveV3UserAccountData(web3, poolAddress, address) {
        const contract = new web3.eth.Contract(aavePool_1.AavePoolAbi, poolAddress);
        const accountData = await contract.methods
            .getUserAccountData(web3.utils.toChecksumAddress(address))
            .call();
        return {
            totalCollateralBase: accountData.totalCollateralBase.toString(),
            totalDebtBase: accountData.totalDebtBase.toString(),
            availableBorrowsBase: accountData.availableBorrowsBase.toString(),
            currentLiquidationThreshold: accountData.currentLiquidationThreshold.toString(),
            ltv: accountData.ltv.toString(),
            healthFactor: accountData.healthFactor.toString()
        };
    }
    async getAaveReserveData(params) {
        const { network, asset, version } = params;
        const poolAddress = (0, aave_1.getAavePoolAddress)(this.chain, network, version);
        if (!poolAddress) {
            throw new Error(`Unsupported Aave pool for chain "${this.chain}", network "${network}", version "${version}".`);
        }
        const { web3 } = await this.getWeb3(network);
        const abi = version === 'v2' ? aavePoolV2_1.AavePoolAbiV2 : aavePool_1.AavePoolAbi;
        const contract = new web3.eth.Contract(abi, poolAddress);
        const reserveData = await contract.methods.getReserveData(web3.utils.toChecksumAddress(asset)).call();
        return { currentVariableBorrowRate: reserveData.currentVariableBorrowRate.toString() };
    }
    async getAaveReserveTokensAddresses(params) {
        const { network, asset, version } = params;
        const poolAddress = (0, aave_1.getAavePoolAddress)(this.chain, network, version);
        if (!poolAddress) {
            throw new Error(`Unsupported Aave pool for chain "${this.chain}", network "${network}", version "${version}".`);
        }
        const { web3 } = await this.getWeb3(network);
        const abi = version === 'v2' ? aavePoolV2_1.AavePoolAbiV2 : aavePool_1.AavePoolAbi;
        const contract = new web3.eth.Contract(abi, poolAddress);
        const reserveData = await contract.methods.getReserveData(web3.utils.toChecksumAddress(asset)).call();
        return { variableDebtTokenAddress: reserveData.variableDebtTokenAddress };
    }
    async getFee(params) {
        let { network } = params;
        const { target = 4, txType } = params;
        const chain = this.chain;
        if (network === 'livenet') {
            network = 'mainnet';
        }
        let cacheKey = `getFee-${chain}-${network}-${target}`;
        if (txType != null) {
            cacheKey += `-type${txType}`;
        }
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            let feerate;
            if (txType?.toString() === '2') {
                const { rpc } = await this.getWeb3(network, { type: 'historical' });
                feerate = await rpc.estimateFee({ nBlocks: target, txType: txType?.toString() });
            }
            else {
                const txs = await transaction_1.EVMTransactionStorage.collection
                    .find({ chain, network, blockHeight: { $gt: 0 } })
                    .project({ gasPrice: 1, blockHeight: 1 })
                    .sort({ blockHeight: -1 })
                    .limit(20 * 200)
                    .toArray();
                const blockGasPrices = txs
                    .map(tx => Number(tx.gasPrice))
                    .filter(gasPrice => gasPrice)
                    .sort((a, b) => b - a);
                const whichQuartile = Math.min(target, 4) || 1;
                const quartileMedian = stats_1.StatsUtil.getNthQuartileMedian(blockGasPrices, whichQuartile);
                const roundedGwei = (quartileMedian / 1e9).toFixed(2);
                const gwei = Number(roundedGwei) || 0;
                feerate = gwei * 1e9;
            }
            return { feerate, blocks: target };
        }, cache_1.CacheStorage.Times.Minute);
    }
    async getPriorityFee(params) {
        let { network } = params;
        const { percentile } = params;
        const chain = this.chain;
        const priorityFeePercentile = percentile || 15;
        if (network === 'livenet') {
            network = 'mainnet';
        }
        const cacheKey = `getFee-${chain}-${network}-priorityFee-${priorityFeePercentile}`;
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            const { rpc } = await this.getWeb3(network);
            const feerate = await rpc.estimateMaxPriorityFee({ percentile: priorityFeePercentile });
            return { feerate: Number(feerate) };
        }, cache_1.CacheStorage.Times.Minute);
    }
    async getWalletBalanceAtTime(params) {
        let { args } = params;
        const { network, wallet, time } = params;
        if (time) {
            if (args) {
                args.time = time;
            }
            else {
                args = { time };
            }
        }
        const hex = args.hex === 'true' || args.hex === '1';
        if (wallet._id === undefined) {
            throw new Error('Wallet balance can only be retrieved for wallets with the _id property');
        }
        let addresses = await this.getWalletAddresses(wallet._id);
        addresses = !args.address ? addresses : addresses.filter(({ address }) => address.toLowerCase() === args.address.toLowerCase());
        const addressBalances = await Promise.all(addresses.map(({ address }) => this.getBalanceForAddress({ chain: this.chain, network, address, args })));
        const balance = addressBalances.reduce((prev, cur) => ({
            unconfirmed: BigInt(prev.unconfirmed) + BigInt(cur.unconfirmed),
            confirmed: BigInt(prev.confirmed) + BigInt(cur.confirmed),
            balance: BigInt(prev.balance) + BigInt(cur.balance)
        }), { unconfirmed: 0n, confirmed: 0n, balance: 0n });
        return {
            unconfirmed: hex ? '0x' + balance.unconfirmed.toString(16) : Number(balance.unconfirmed),
            confirmed: hex ? '0x' + balance.confirmed.toString(16) : Number(balance.confirmed),
            balance: hex ? '0x' + balance.balance.toString(16) : Number(balance.balance)
        };
    }
    async getBalanceForAddress(params) {
        const { chain, network, address, args } = params;
        const { web3 } = await this.getWeb3(network, { type: args?.time ? 'historical' : 'realtime' });
        const tokenAddress = args?.tokenAddress;
        const addressLower = address.toLowerCase();
        const hex = args?.hex === 'true' || args?.hex === '1';
        let blockNumber = 'latest';
        if (args?.time) {
            const block = await this.getBlockBeforeTime({ chain, network, time: args.time });
            if (!block) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            blockNumber = block.height;
        }
        const cacheKey = tokenAddress
            ? `getBalanceForAddress-${chain}-${network}-${addressLower}-${tokenAddress.toLowerCase()}`
            : `getBalanceForAddress-${chain}-${network}-${addressLower}`;
        const balance = await cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            if (tokenAddress) {
                const token = new web3.eth.Contract(erc20_1.ERC20Abi, tokenAddress);
                const balance = await token.methods.balanceOf(address).call({}, blockNumber);
                const numberBalance = '0x' + BigInt(balance).toString(16);
                return { confirmed: numberBalance, unconfirmed: '0x0', balance: numberBalance };
            }
            else {
                const balance = await web3.eth.getBalance(address, blockNumber);
                const numberBalance = '0x' + BigInt(balance).toString(16);
                return { confirmed: numberBalance, unconfirmed: '0x0', balance: numberBalance };
            }
        }, args?.time ? cache_1.CacheStorage.Times.None : cache_1.CacheStorage.Times.Minute);
        return {
            confirmed: hex ? balance.confirmed : Number(balance.confirmed),
            unconfirmed: hex ? balance.unconfirmed : Number(balance.unconfirmed),
            balance: hex ? balance.balance : Number(balance.balance)
        };
    }
    async getLocalTip({ chain, network }) {
        return block_1.EVMBlockStorage.getLocalTip({ chain, network });
    }
    async getReceipt(network, txid) {
        const { web3 } = await this.getWeb3(network, { type: 'historical' });
        const receipt = await web3.eth.getTransactionReceipt(txid);
        return crypto_wallet_core_1.Utils.BI.scrubBigIntsInObject(receipt);
    }
    async populateReceipt(tx) {
        if (!tx.receipt) {
            const receipt = await this.getReceipt(tx.network, tx.txid);
            if (receipt) {
                const fee = Number(BigInt(receipt.gasUsed) * BigInt(tx.gasPrice));
                await transaction_1.EVMTransactionStorage.collection.updateOne({ _id: tx._id }, { $set: { receipt, fee } });
                tx.receipt = receipt;
                tx.fee = fee;
            }
        }
        // logs can be very large and are not currently needed for any use case in this codebase.
        delete tx.receipt?.logs;
        return tx;
    }
    populateEffects(tx) {
        if (!tx.effects || (tx.effects && tx.effects.length == 0)) {
            tx.effects = transaction_1.EVMTransactionStorage.getEffects(tx);
        }
        return tx;
    }
    populateEffectsForAddresses(tx, addresses) {
        tx.effects = transaction_1.EVMTransactionStorage.getEffectsForAddresses(tx, addresses);
        return tx;
    }
    async getTransaction(params) {
        try {
            params.network = params.network.toLowerCase();
            const { chain, network, txId } = params;
            if (typeof txId !== 'string') {
                throw new Error('Missing required param: txId');
            }
            if (!chain) {
                throw new Error('Missing required param: chain');
            }
            if (!network) {
                throw new Error('Missing required param: network');
            }
            const tx = await this._getTransaction(params);
            let { found } = tx;
            const { tipHeight } = tx;
            if (found) {
                let confirmations = 0;
                if (found.blockHeight && found.blockHeight >= 0) {
                    confirmations = tipHeight - found.blockHeight + 1;
                }
                found = await this.populateReceipt(found);
                // Add effects to old db entries
                found = this.populateEffects(found);
                const convertedTx = transaction_1.EVMTransactionStorage._apiTransform(found, { object: true });
                return { ...convertedTx, confirmations };
            }
        }
        catch (err) {
            console.error(err);
        }
        return undefined;
    }
    async _getTransaction(params) {
        const { chain, network, txId } = params;
        const query = { chain, network, txid: txId };
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        const found = await transaction_1.EVMTransactionStorage.collection.findOne(query);
        return { tipHeight, found };
    }
    async broadcastTransaction(params) {
        const { network, rawTx } = params;
        const { web3 } = await this.getWeb3(network, { type: 'realtime' });
        const rawTxs = typeof rawTx === 'string' ? [rawTx] : rawTx;
        const txids = new Array();
        for (const tx of rawTxs) {
            const txid = await new Promise((resolve, reject) => {
                web3.eth
                    .sendSignedTransaction(tx)
                    .on('transactionHash', resolve)
                    .on('error', reject)
                    .catch(e => {
                    logger_1.default.error('%o', e);
                    reject(e);
                });
            });
            txids.push(txid);
        }
        return txids.length === 1 ? txids[0] : txids;
    }
    async streamAddressTransactions(params) {
        return new Promise(async (resolve, reject) => {
            try {
                await this._buildAddressTransactionsStream(params);
                return resolve();
            }
            catch (err) {
                return reject(err);
            }
        });
    }
    async _buildAddressTransactionsStream(params) {
        const { req, res, args, chain, network, address } = params;
        const { limit, /* since,*/ tokenAddress } = args;
        if (!args.tokenAddress) {
            const query = {
                $or: [
                    { chain, network, from: address },
                    { chain, network, to: address },
                    { chain, network, 'internal.action.to': address }, // Retained for old db entries
                    { chain, network, 'effects.to': address }
                ]
            };
            // NOTE: commented out since and paging for now b/c they were causing extra long query times on insight.
            // The case where an address has >1000 txns is an edge case ATM and can be addressed later
            storage_1.Storage.apiStreamingFind(transaction_1.EVMTransactionStorage, query, { limit /* since, paging: '_id'*/ }, req, res);
        }
        else {
            try {
                const tokenTransfers = await this.getErc20Transfers(network, address, tokenAddress, args);
                res.json(tokenTransfers);
            }
            catch (err) {
                logger_1.default.error('Error streaming address transactions: %o', err.stack || err.message || err);
                throw err;
            }
        }
    }
    async streamTransactions(params) {
        const { chain, network, req, res, args } = params;
        const { blockHash, blockHeight } = args;
        if (!chain || !network) {
            throw new Error('Missing chain or network');
        }
        const query = {
            chain,
            network: network.toLowerCase()
        };
        if (blockHeight !== undefined) {
            query.blockHeight = Number(blockHeight);
        }
        if (blockHash !== undefined) {
            query.blockHash = blockHash;
        }
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        return storage_1.Storage.apiStreamingFind(transaction_1.EVMTransactionStorage, query, args, req, res, t => {
            let confirmations = 0;
            if (t.blockHeight !== undefined && t.blockHeight >= 0) {
                confirmations = tipHeight - t.blockHeight + 1;
            }
            // Add effects to old db entries
            if (!t.effects || (t.effects && t.effects.length == 0)) {
                t.effects = transaction_1.EVMTransactionStorage.getEffects(t);
            }
            const convertedTx = transaction_1.EVMTransactionStorage._apiTransform(t, { object: true });
            return JSON.stringify({ ...convertedTx, confirmations });
        });
    }
    async getWalletBalance(params) {
        const { network, args, wallet } = params;
        const hex = args.hex === 'true' || args.hex === '1';
        if (wallet._id === undefined) {
            throw new Error('Wallet balance can only be retrieved for wallets with the _id property');
        }
        let addresses = await this.getWalletAddresses(wallet._id);
        addresses = !args.address ? addresses : addresses.filter(({ address }) => address.toLowerCase() === args.address.toLowerCase());
        const addressBalances = await Promise.all(addresses.map(({ address }) => this.getBalanceForAddress({ chain: this.chain, network, address, args })));
        const balance = addressBalances.reduce((prev, cur) => ({
            unconfirmed: BigInt(prev.unconfirmed) + BigInt(cur.unconfirmed),
            confirmed: BigInt(prev.confirmed) + BigInt(cur.confirmed),
            balance: BigInt(prev.balance) + BigInt(cur.balance)
        }), { unconfirmed: 0n, confirmed: 0n, balance: 0n });
        return {
            unconfirmed: hex ? '0x' + balance.unconfirmed.toString(16) : Number(balance.unconfirmed),
            confirmed: hex ? '0x' + balance.confirmed.toString(16) : Number(balance.confirmed),
            balance: hex ? '0x' + balance.balance.toString(16) : Number(balance.balance)
        };
    }
    getWalletTransactionQuery(params) {
        const { chain, network, wallet, args } = params;
        const query = {
            chain,
            network,
            wallets: wallet._id,
            'wallets.0': { $exists: true },
            blockHeight: { $gt: -3 } // Exclude invalid transactions
        };
        if (args) {
            if (args.startBlock || args.endBlock) {
                query.$or = [];
                if (args.includeMempool) {
                    query.$or.push({ blockHeight: -1 /* SpentHeightIndicators.pending */ });
                }
                const blockRangeQuery = {};
                if (args.startBlock) {
                    blockRangeQuery.$gte = Number(args.startBlock);
                }
                if (args.endBlock) {
                    blockRangeQuery.$lte = Number(args.endBlock);
                }
                query.$or.push({ blockHeight: blockRangeQuery });
            }
            else {
                if (args.startDate) {
                    const startDate = new Date(args.startDate);
                    if (startDate.getTime()) {
                        query.blockTimeNormalized = { $gte: new Date(args.startDate) };
                    }
                }
                if (args.endDate) {
                    const endDate = new Date(args.endDate);
                    if (endDate.getTime()) {
                        query.blockTimeNormalized = query.blockTimeNormalized || {};
                        query.blockTimeNormalized.$lt = new Date(args.endDate);
                    }
                }
            }
            if (args.includeInvalidTxs) {
                delete query.blockHeight;
            }
        }
        return query;
    }
    async streamWalletTransactions(params) {
        return new Promise(async (resolve, reject) => {
            const { network, wallet, req, res, args } = params;
            const { web3 } = await this.getWeb3(network);
            args.tokenAddress = args.tokenAddress ? web3.utils.toChecksumAddress(args.tokenAddress) : undefined;
            let transactionStream = new streamWithEventPipe_1.TransformWithEventPipe({ objectMode: true, passThrough: true });
            const walletAddresses = (await this.getWalletAddresses(wallet._id)).map(waddres => waddres.address);
            if (walletAddresses.length === 0) {
                res.status(400).send('No addresses found for wallet');
                return resolve();
            }
            const ethTransactionTransform = new transform_1.EVMListTransactionsStream(walletAddresses, args.tokenAddress);
            const populateReceipt = new populateReceiptTransform_1.PopulateReceiptTransform(this);
            const populateEffects = new populateEffectsTransform_1.PopulateEffectsForAddressTransform(this, walletAddresses);
            const streamParams = {
                transactionStream,
                populateEffects,
                walletAddresses
            };
            transactionStream = await this._buildWalletTransactionsStream(params, streamParams);
            if (!args.tokenAddress && wallet._id) {
                const internalTxTransform = new internalTxTransform_1.InternalTxRelatedFilterTransform(web3, wallet._id);
                transactionStream = transactionStream.eventPipe(internalTxTransform);
            }
            transactionStream = transactionStream
                .eventPipe(populateReceipt)
                .eventPipe(ethTransactionTransform);
            try {
                const result = await apiStream_1.ExternalApiStream.onStream(transactionStream, req, res, { jsonl: true });
                if (!result?.success) {
                    logger_1.default.error('Error mid-stream (streamWalletTransactions): %o', result.error?.log || result.error);
                }
                return resolve();
            }
            catch (err) {
                return reject(err);
            }
        });
    }
    async _buildWalletTransactionsStream(params, streamParams) {
        const query = this.getWalletTransactionQuery(params);
        let { transactionStream } = streamParams;
        const { populateEffects } = streamParams;
        // Store cursor reference for cleanup
        const cursor = transaction_1.EVMTransactionStorage.collection
            .find(query)
            .sort({ blockTimeNormalized: 1 })
            .addCursorFlag('noCursorTimeout', true);
        // Add cleanup handlers when client disconnects
        let cursorClosed = false;
        const cleanupCursor = () => {
            if (!cursorClosed) {
                cursorClosed = true;
                try {
                    cursor.close();
                }
                catch {
                    // Cursor might already be closed, ignore
                }
            }
        };
        const { req, res } = params;
        req.on('close', cleanupCursor);
        res.on('close', cleanupCursor);
        // Pipe cursor to transform stream
        transactionStream = cursor.pipe(new streamWithEventPipe_1.TransformWithEventPipe({ objectMode: true, passThrough: true }));
        transactionStream = transactionStream.eventPipe(populateEffects); // For old db entries
        if (params.args.tokenAddress) {
            const erc20Transform = new erc20Transform_1.Erc20RelatedFilterTransform(params.args.tokenAddress);
            transactionStream = transactionStream.eventPipe(erc20Transform);
        }
        return transactionStream;
    }
    async getErc20Transfers(network, address, tokenAddress, args = {}) {
        const token = await this.erc20For(network, tokenAddress);
        let windowSize = 100n;
        const { web3 } = await this.getWeb3(network);
        const tip = await web3.eth.getBlockNumber();
        if (isNaN(args.startBlock) || isNaN(args.endBlock)) {
            throw new Error('startBlock and endBlock must be numbers');
        }
        let endBlock = args.endBlock == null ? null : BigInt(args.endBlock);
        let startBlock = args.startBlock == null ? null : BigInt(args.startBlock);
        // If endBlock or startBlock is negative, it is a block offset from the tip
        if (endBlock < 0n) {
            endBlock = tip + endBlock;
        }
        if (startBlock < 0n) {
            startBlock = tip + startBlock;
        }
        endBlock = crypto_wallet_core_1.Utils.BI.min([endBlock ?? tip, tip]);
        startBlock = crypto_wallet_core_1.Utils.BI.max([startBlock != null ? startBlock : endBlock - 10000n, 0n]);
        if (startBlock > endBlock) {
            throw new Error('startBlock cannot be greater than endBlock');
        }
        else if (endBlock - startBlock > 10000n) {
            throw new Error('Cannot scan more than 10000 blocks at a time. Please limit your search with startBlock and endBlock');
        }
        windowSize = crypto_wallet_core_1.Utils.BI.min([windowSize, endBlock - startBlock]);
        const tokenTransfers = [];
        while (windowSize > 0n) {
            const [sent, received] = await Promise.all([
                token.getPastEvents('Transfer', {
                    filter: { _from: address },
                    fromBlock: endBlock - windowSize,
                    toBlock: endBlock
                }),
                token.getPastEvents('Transfer', {
                    filter: { _to: address },
                    fromBlock: endBlock - windowSize,
                    toBlock: endBlock
                })
            ]);
            tokenTransfers.push(...this.convertTokenTransfers([...sent, ...received]));
            endBlock -= windowSize + 1n;
            windowSize = crypto_wallet_core_1.Utils.BI.min([windowSize, endBlock - startBlock]);
        }
        return tokenTransfers;
    }
    convertTokenTransfers(tokenTransfers) {
        return tokenTransfers.map(this.convertTokenTransfer);
    }
    convertTokenTransfer(transfer) {
        const { blockHash, blockNumber, transactionHash, returnValues, transactionIndex } = transfer;
        return {
            blockHash,
            blockNumber: Number(blockNumber),
            transactionHash,
            transactionIndex: Number(transactionIndex),
            hash: transactionHash,
            from: returnValues['_from'],
            to: returnValues['_to'],
            value: Number(returnValues['_value'])
        };
    }
    async getAccountNonce(network, address) {
        const { web3 } = await this.getWeb3(network, { type: 'realtime' });
        const count = await web3.eth.getTransactionCount(address);
        return Number(count);
    }
    async getWalletTokenTransactions(network, walletId, tokenAddress, args) {
        const addresses = await this.getWalletAddresses(walletId);
        const batches = await Promise.all(addresses.map(walletAddress => this.getErc20Transfers(network, walletAddress.address, tokenAddress, args)));
        return batches.flat().sort((tx1, tx2) => Number(BigInt(tx1.blockNumber) - BigInt(tx2.blockNumber)));
    }
    async estimateGas(params) {
        let { value } = params;
        const { network, from, data, /* gasPrice */ to } = params;
        const { web3 } = await this.getWeb3(network, { type: 'realtime' });
        const dataDecoded = transaction_1.EVMTransactionStorage.abiDecode(data);
        if (dataDecoded && dataDecoded.type === 'INVOICE' && dataDecoded.name === 'pay') {
            value = dataDecoded.params[0].value;
            // gasPrice = dataDecoded.params[1].value;
        }
        else if (data && data.type === 'MULTISEND') {
            let method, gasLimit;
            const contract = await this.getMultisendContract(network, to);
            const addresses = web3.eth.abi.decodeParameter('address[]', data.addresses);
            const amounts = web3.eth.abi.decodeParameter('uint256[]', data.amounts);
            switch (data.method) {
                case 'sendErc20':
                    method = contract.methods.sendErc20(data.tokenAddress, addresses, amounts);
                    gasLimit = method ? await method.estimateGas({ from }) : undefined;
                    break;
                case 'sendEth':
                    method = contract.methods.sendEth(addresses, amounts);
                    gasLimit = method ? await method.estimateGas({ from, value }) : undefined;
                    break;
                default:
                    break;
            }
            return Number(gasLimit);
        }
        let _value;
        if (data) {
            // Gas estimation might fail with `insufficient funds` if value is higher than balance for a normal send.
            // We want this method to give a blind fee estimation, though, so we should not include the value
            // unless it's needed for estimating smart contract execution.
            _value = crypto_wallet_core_1.Utils.toHex(value);
        }
        const opts = {
            method: 'eth_estimateGas',
            params: [
                {
                    data,
                    to: to && to.toLowerCase(),
                    from: from && from.toLowerCase(),
                    // gasPrice: Utils.toHex(gasPrice), // Setting this lower than the baseFee of the last block will cause an error. Better to just leave it out.
                    value: _value
                }
            ],
            jsonrpc: '2.0',
            id: 'bitcore-' + Date.now()
        };
        const provider = web3.currentProvider;
        const response = await provider.request(opts);
        if (!response.result)
            throw new Error(JSON.stringify(response.error || response));
        return Number(response.result);
    }
    async getBlocks(params) {
        const { tipHeight, blocks } = await this._getBlocks(params);
        const blockTransform = (b) => {
            let confirmations = 0;
            if (b.height && b.height >= 0) {
                confirmations = tipHeight - b.height + 1;
            }
            const convertedBlock = block_1.EVMBlockStorage._apiTransform(b, { object: true });
            return { ...convertedBlock, confirmations };
        };
        return blocks.map(blockTransform);
    }
    async _getBlocks(params) {
        const { query, options } = this.getBlocksQuery(params);
        let cursor = block_1.EVMBlockStorage.collection.find(query, options).addCursorFlag('noCursorTimeout', true);
        if (options.sort) {
            cursor = cursor.sort(options.sort);
        }
        const blocks = await cursor.toArray();
        const tip = await this.getLocalTip(params);
        const tipHeight = tip ? tip.height : 0;
        return { tipHeight, blocks };
    }
    async updateWallet(params) {
        const { chain, network } = params;
        const addressBatches = (0, utils_1.partition)(params.addresses, 500);
        for (const addressBatch of addressBatches) {
            const walletAddressInserts = addressBatch.map(address => {
                return {
                    insertOne: {
                        document: { chain, network, wallet: params.wallet._id, address, processed: false }
                    }
                };
            });
            try {
                await walletAddress_1.WalletAddressStorage.collection.bulkWrite(walletAddressInserts);
            }
            catch (err) {
                if (err.code !== 11000) {
                    throw err;
                }
            }
            const addressBatchLC = addressBatch.map(address => address.toLowerCase());
            await transaction_1.EVMTransactionStorage.collection.updateMany({
                $or: [
                    { chain, network, from: { $in: addressBatch } },
                    { chain, network, to: { $in: addressBatch } },
                    { chain, network, 'internal.action.to': { $in: addressBatchLC } }, // Support old db entries
                    { chain, network, 'calls.to': { $in: addressBatchLC } }, // Support old db entries
                    {
                        chain,
                        network,
                        'calls.abiType.type': 'ERC20',
                        'calls.abiType.name': { $in: ['transfer', 'transferFrom'] },
                        'calls.abiType.params.type': 'address',
                        'calls.abiType.params.value': { $in: addressBatchLC }
                    },
                    { chain, network, 'effects.to': { $in: addressBatch } },
                    { chain, network, 'effects.from': { $in: addressBatch } },
                ]
            }, { $addToSet: { wallets: params.wallet._id } });
            await walletAddress_1.WalletAddressStorage.collection.updateMany({ chain, network, address: { $in: addressBatch }, wallet: params.wallet._id }, { $set: { processed: true } });
        }
    }
    async getBlocksRange(params) {
        const { chain, network, chainId, sinceBlock, args = {} } = params;
        let { blockId } = params;
        let { startDate, endDate, limit = 10 } = args;
        const { date, sort = { height: -1 } } = args;
        const query = {};
        if (!chain || !network) {
            throw new Error('Missing required chain and/or network param');
        }
        // limit - 1 because startBlock is inclusive; ensure limit is >= 0
        limit = Math.max(limit - 1, 0);
        let height = null;
        if (blockId && blockId.length < 64) {
            height = parseInt(blockId, 10);
            if (isNaN(height) || height.toString(10) != blockId) {
                throw new Error('invalid block id provided');
            }
            blockId = undefined;
        }
        if (date) {
            startDate = new Date(date);
            endDate = new Date(date);
            endDate.setDate(endDate.getDate() + 1);
        }
        if (startDate || endDate) {
            if (startDate) {
                query.startBlock = await this._getBlockNumberByDate({ date: startDate, chainId }) || 0;
            }
            if (endDate) {
                query.endBlock = await this._getBlockNumberByDate({ date: endDate, chainId }) || 0;
            }
        }
        // Get range
        if (sinceBlock) {
            const height = Number(sinceBlock);
            if (isNaN(height) || height.toString(10) != sinceBlock) {
                throw new Error('invalid block id provided');
            }
            const { web3 } = await this.getWeb3(network);
            const tipHeight = Number(await web3.eth.getBlockNumber());
            if (tipHeight < height) {
                return [];
            }
            query.endBlock = query.endBlock ?? tipHeight;
            query.startBlock = query.startBlock ?? query.endBlock - limit;
        }
        else if (blockId) {
            const { web3 } = await this.getWeb3(network);
            const blk = await web3.eth.getBlock(blockId);
            if (!blk || blk.number == null) {
                throw new Error(`Could not get block ${blockId}`);
            }
            height = Number(blk.number);
        }
        if (height != null) {
            query.startBlock = height;
            query.endBlock = height + limit;
        }
        if (query.startBlock == null || query.endBlock == null) {
            // Calaculate range with options
            const { web3 } = await this.getWeb3(network);
            const tipHeight = Number(await web3.eth.getBlockNumber());
            query.endBlock = query.endBlock ?? tipHeight;
            query.startBlock = query.startBlock ?? query.endBlock - limit;
        }
        if (query.endBlock - query.startBlock > limit) {
            query.endBlock = query.startBlock + limit;
        }
        const r = (0, utils_1.range)(query.startBlock, query.endBlock + 1); // +1 since range is [start, end)
        if (sort?.height === -1 && query.startBlock < query.endBlock) {
            return r.reverse();
        }
        return r;
    }
    async _getBlockNumberByDate(params) {
        const { date, network } = params;
        const block = await block_1.EVMBlockStorage.collection.findOne({ chain: this.chain, network, timeNormalized: { $gte: date } }, { sort: { timeNormalized: 1 } });
        return block?.height;
    }
    async getChainId({ network }) {
        const { web3 } = await this.getWeb3(network);
        return web3.eth.getChainId();
    }
    async getCoinsForTx() {
        return {
            inputs: [],
            outputs: []
        };
    }
}
exports.BaseEVMStateProvider = BaseEVMStateProvider;
BaseEVMStateProvider.rpcs = {};
BaseEVMStateProvider.rpcIndicies = {};
BaseEVMStateProvider.rpcInitialized = {};
__decorate([
    decorators_1.historical
], BaseEVMStateProvider.prototype, "getFee", null);
__decorate([
    decorators_1.historical,
    decorators_1.internal
], BaseEVMStateProvider.prototype, "streamTransactions", null);
__decorate([
    decorators_1.realtime
], BaseEVMStateProvider.prototype, "getWalletBalance", null);
__decorate([
    decorators_1.realtime
], BaseEVMStateProvider.prototype, "getAccountNonce", null);
__decorate([
    decorators_1.realtime
], BaseEVMStateProvider.prototype, "estimateGas", null);
//# sourceMappingURL=csp.js.map