"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvmDecoder = void 0;
const async_tasks_1 = require("@splunkdlt/async-tasks");
const cache_1 = require("@splunkdlt/cache");
const debug_logging_1 = require("@splunkdlt/debug-logging");
const contract_1 = require("./abi/contract");
const repo_1 = require("./abi/repo");
const client_1 = require("./eth/client");
const http_1 = require("./eth/http");
const requests_1 = require("./eth/requests");
const bn_1 = require("./utils/bn");
const classification_1 = require("./utils/classification");
const error_1 = require("./utils/error");
const format_1 = require("./utils/format");
const obj_1 = require("./utils/obj");
const { info, warn, error } = debug_logging_1.createModuleDebug('evmdecoder:main');
const DEFAULT_CONFIG = {
    eth: {
        http: {
            timeout: 60000,
            validateCertificate: false,
            requestKeepAlive: true,
            maxSockets: 256
        },
        client: {
            maxBatchSize: 100,
            maxBatchTime: 0,
            individualReceipts: true,
            maxRetryTime: 10000
        }
    },
    abi: {
        abiFileExtension: '.json',
        directory: './abis',
        searchRecursive: true,
        fingerprintContracts: true,
        requireContractMatch: false,
        decodeAnonymous: true,
        reconcileStructShapeFromTuples: true
    },
    contractInfo: {
        maxCacheEntries: 25000
    },
    logging: {}
};
function isRawFullBlock(b) {
    const rfb = b;
    return (rfb.block != null && rfb.transactions != null);
}
const addressInfo = (contractInfo) => contractInfo !== null && contractInfo !== void 0 ? contractInfo : undefined;
class EvmDecoder {
    constructor(config) {
        this.resources = [];
        this.config = obj_1.deepMerge(DEFAULT_CONFIG, config);
        this.abortHandle = new async_tasks_1.AbortHandle();
        this.waitAfterFailure = async_tasks_1.exponentialBackoff({ min: 0, max: this.config.eth.client.maxRetryTime });
        this.abiRepo = new repo_1.AbiRepository(this.config.abi, this.config.logging);
        this.addResource(this.abiRepo);
        this.contractInfoCache = new cache_1.LRUCache({
            maxSize: this.config.contractInfo.maxCacheEntries
        });
        const transport = new http_1.HttpTransport(this.config.eth.url, this.config.eth.http);
        this.ethClient =
            this.config.eth.client.maxBatchSize > 1
                ? new client_1.BatchedEthereumClient(transport, {
                    maxBatchSize: this.config.eth.client.maxBatchSize,
                    maxBatchTime: this.config.eth.client.maxBatchTime
                })
                : new client_1.EthereumClient(transport);
        const classificationResources = {
            ethClient: this.ethClient,
            abiRepo: this.abiRepo,
            contractInfoCache: this.contractInfoCache
        };
        this.classification = new classification_1.Classification(classificationResources, this.config.logging);
        this.contractResources = {
            ethClient: this.ethClient,
            abiRepo: this.abiRepo,
            contractInfoCache: this.contractInfoCache,
            classification: this.classification
        };
    }
    async initialize() {
        await this.abiRepo.initialize();
    }
    async _decodeFunctionCall({ input, address, contractInfo }) {
        const decoded = this.abiRepo.decodeFunctionCall(input, {
            contractAddress: address,
            contractFingerprint: contractInfo != null ? contractInfo.fingerprint : undefined
        });
        if (address != null && decoded != null && contractInfo != null) {
            try {
                return this.classification.getExtraData(address, decoded, contractInfo, input);
            }
            catch (e) {
                warn('could not get extra data for transaction');
            }
        }
        return decoded;
    }
    async decodeFunctionCall({ input, address }) {
        const contractInfo = address != null ? await this.contractInfo({ address }) : undefined;
        const decoded = await this._decodeFunctionCall({ input, address, contractInfo });
        return decoded;
    }
    async decodeFunctionCallV2({ input, address }) {
        const contractInfo = address != null ? await this.contractInfo({ address }) : undefined;
        const decoded = await this._decodeFunctionCall({ input, address, contractInfo });
        return {
            decoded,
            contractInfo
        };
    }
    async contractInfo({ address }) {
        return contract_1.contractInfo({
            address,
            resources: this.contractResources
        });
    }
    async getBlock(blockNumber, decode = true) {
        const block = await this.ethClient.request(requests_1.getBlock(blockNumber));
        if (decode) {
            return this.processBlock(block);
        }
        return block;
    }
    async getFullBlock(blockNumber, decode = true) {
        const block = await this.ethClient.request(requests_1.getBlock(blockNumber));
        if (decode) {
            return this.processBlock(block);
        }
        return this.processRawFullBlock(block);
    }
    async processRawFullBlock(block) {
        // const uncles = await this.ethClient.requestBatch()
        // last uncle block https://etherscan.io/uncle/0xf4af15465ca81e65866c6e64cbc446b735a06fb2118dda69a7c21d4ab0b1e217
        const allReceipts = await this.ethClient.requestBatch(block.transactions.map(txn => typeof txn === 'string' ? requests_1.getTransactionReceipt(txn) : requests_1.getTransactionReceipt(txn.hash)));
        const receiptMap = allReceipts.reduce((map, receipt) => {
            map[receipt.transactionHash] = receipt;
            return map;
        }, {});
        const { transactions: txns, ...remainingBlockProps } = block;
        const rawBlockSlim = {
            ...remainingBlockProps,
            transactions: txns.map(txn => {
                if (typeof txn === 'string') {
                    return txn;
                }
                return txn.hash;
            })
        };
        const rawFullBlock = {
            block: rawBlockSlim,
            transactions: txns.map((transaction) => {
                var _a;
                return ({
                    transaction,
                    receipt: (_a = receiptMap[transaction.hash]) !== null && _a !== void 0 ? _a : undefined
                });
            })
        };
        return rawFullBlock;
    }
    async getBlocks(blockNumbers, decode = true) {
        const blocks = await this.ethClient
            .requestBatch(blockNumbers.map(blockNumber => requests_1.getBlock(blockNumber)))
            .catch(e => Promise.reject(new Error(`Failed to request batch of blocks ${blockNumbers.join(', ')}: ${e}`)));
        if (decode) {
            return Promise.all(blocks.map(b => this.processBlock(b)));
        }
        return blocks;
    }
    async getFullBlocks(blockNumbers, decode = true) {
        if (decode) {
            return this.getBlocks(blockNumbers, true);
        }
        const blocks = await this.getBlocks(blockNumbers, false);
        const fullBlocks = [];
        for (const b of blocks) {
            const block = await this.processRawFullBlock(b);
            fullBlocks.push(block);
        }
        return fullBlocks;
    }
    async getPendingTransaction(hash, decode = true) {
        try {
            const transaction = await this.ethClient.request(requests_1.getTransaction(hash), { ignoreErrors: true });
            if (decode) {
                return this.processPendingTransaction(transaction);
            }
            return transaction;
        }
        catch (e) {
            return undefined;
        }
    }
    async getTransaction(hash, decode = true) {
        const transaction = await this.ethClient.request(requests_1.getTransaction(hash));
        if (decode) {
            return this.processTransaction(transaction, true);
        }
        return transaction;
    }
    async getTransactionReceipt(hash, decode = true) {
        var _a, _b;
        const receipt = await this.ethClient.request(requests_1.getTransactionReceipt(hash));
        if (receipt && decode) {
            return Promise.all((_b = (_a = receipt === null || receipt === void 0 ? void 0 : receipt.logs) === null || _a === void 0 ? void 0 : _a.map((l) => this.processTransactionLog(l))) !== null && _b !== void 0 ? _b : []);
        }
        if (receipt) {
            return receipt;
        }
        return [];
    }
    async processRawBlock(rawBlock) {
        const block = format_1.formatBlock(rawBlock);
        let receipts = [];
        let individualReceipts = this.config.eth.client.individualReceipts;
        if (!individualReceipts) {
            try {
                receipts = await async_tasks_1.retry(() => this.ethClient.request(requests_1.getBlockReceipts(block.number)), {
                    attempts: 3,
                    waitBetween: this.waitAfterFailure,
                    taskName: `getting receipts for block ${bn_1.bigIntToNumber(block.number)}`,
                    abortHandle: this.abortHandle,
                    warnOnError: true,
                    onRetry: (attempt) => warn('Retrying to get receipts for block %s (attempt %d)', bn_1.bigIntToNumber(block.number), attempt)
                });
            }
            catch (e) {
                warn('unable to get receipts for full block %d, switching to individualReceipts mode', bn_1.bigIntToNumber(block.number));
                individualReceipts = true;
            }
        }
        const transactions = rawBlock.transactions.length === 0 ? [] : await this.abortHandle.race(Promise.all(rawBlock.transactions.map(tx => this.processTransaction(tx, individualReceipts, !individualReceipts
            ? receipts.filter(r => r.transactionHash === tx.hash)[0]
            : undefined))));
        return {
            block,
            transactions
        };
    }
    processRawFullTransaction(txn) {
        const { transaction: rawTransaction, receipt } = txn;
        return this.processTransaction(rawTransaction, false, receipt);
    }
    async processRawBlockWithTransactions(rawFullBlock) {
        const { block: rawBlock, transactions: rawTransactions } = rawFullBlock;
        const block = format_1.formatBlock(rawBlock);
        const transactions = rawTransactions.length === 0 ? [] : await this.abortHandle.race(Promise.all(rawTransactions.map(rawTxn => this.processRawFullTransaction(rawTxn))));
        return {
            block,
            transactions
        };
    }
    async processBlock(rawBlock) {
        if (isRawFullBlock(rawBlock)) {
            return this.processRawBlockWithTransactions(rawBlock);
        }
        else {
            return this.processRawBlock(rawBlock);
        }
    }
    async processTransaction(rawTx, fetchReceipts, rawReceipt = undefined) {
        var _a;
        if (typeof rawTx === 'string') {
            warn('Received raw transaction %s from block %d');
            throw new error_1.RuntimeError(`Received raw transaction`);
        }
        const [receipt, toInfo, fromInfo] = await Promise.all([
            fetchReceipts ? this.ethClient.request(requests_1.getTransactionReceipt(rawTx.hash)) : rawReceipt,
            rawTx.to != null
                ? contract_1.contractInfo({
                    address: rawTx.to,
                    resources: this.contractResources
                })
                : undefined,
            rawTx.from != null
                ? contract_1.contractInfo({
                    address: rawTx.from,
                    resources: this.contractResources
                })
                : undefined
        ]);
        const contractAddressInfo = (receipt === null || receipt === void 0 ? void 0 : receipt.contractAddress) != null
            ? await contract_1.contractInfo({
                address: receipt.contractAddress,
                resources: this.contractResources
            })
            : undefined;
        const callInfo = toInfo && toInfo.isContract
            ? await this.decodeFunctionCall({ input: rawTx.input, address: rawTx.to })
            : undefined;
        const transaction = format_1.formatTransaction(rawTx, receipt, addressInfo(fromInfo), addressInfo(toInfo), addressInfo(contractAddressInfo), callInfo);
        const logEvents = toInfo && toInfo.isContract
            ? await Promise.all((_a = 
            //@ts-ignore
            this.getLogs(receipt).map((l) => this.processTransactionLog(l))) !== null && _a !== void 0 ? _a : [])
            : [];
        return {
            transaction,
            logEvents
        };
    }
    getLogs(receipt) {
        var _a;
        if (receipt == null || receipt.logs == null) {
            return [];
        }
        return (_a = receipt.logs) !== null && _a !== void 0 ? _a : [];
    }
    async processPendingTransaction(rawTx) {
        if (typeof rawTx === 'string') {
            warn('Received raw transaction %s');
            throw new error_1.RuntimeError(`Received raw transaction`);
        }
        const toInfo = rawTx.to != null
            ? await contract_1.contractInfo({
                address: rawTx.to,
                resources: this.contractResources
            })
            : undefined;
        const callInfo = toInfo && toInfo.isContract
            ? await this.decodeFunctionCall({ input: rawTx.input, address: rawTx.to })
            : undefined;
        return format_1.formatPendingTransaction(rawTx, 'pending', undefined, addressInfo(toInfo), callInfo);
    }
    async decodeLogEvent(evt) {
        const { address } = evt;
        const eventContractInfo = await contract_1.contractInfo({
            address,
            resources: this.contractResources
        });
        const contractFingerprint = eventContractInfo == null ? undefined : eventContractInfo.fingerprint;
        const decodedEventData = (evt.topics != null && Array.isArray(evt.topics) && evt.topics.length > 0)
            ? this.abiRepo.decodeLogEvent(evt, {
                contractAddress: address,
                contractFingerprint
            }) : undefined;
        return { eventContractInfo, decodedEventData };
    }
    async processTransactionLog(evt) {
        const { eventContractInfo, decodedEventData } = await this.decodeLogEvent(evt);
        if (evt.address != null && decodedEventData != null && eventContractInfo != null) {
            try {
                const decodedExtra = await this.classification.getExtraData(evt.address, decodedEventData, eventContractInfo);
                return format_1.formatLogEvent(evt, addressInfo(eventContractInfo), decodedExtra);
            }
            catch (e) {
                warn('could not get extra data for transaction log');
            }
        }
        return format_1.formatLogEvent(evt, addressInfo(eventContractInfo), decodedEventData);
    }
    async getLatestBlockNumber() {
        return this.ethClient.request(requests_1.blockNumber());
    }
    addResource(r) {
        this.resources.push(r);
        return r;
    }
}
exports.EvmDecoder = EvmDecoder;
