"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.contractInfo = exports.getContractInfo = exports.computeContractFingerprint = exports.extractFunctionsAndEvents = void 0;
const debug_logging_1 = require("@splunkdlt/debug-logging");
const cache_1 = require("@splunkdlt/cache");
const requests_1 = require("../eth/requests");
const wasm_1 = require("./wasm");
const { info, debug, trace, warn } = debug_logging_1.createModuleDebug('abi:contract');
/**
 * Find function and event signature hashes in the EVM bytecode and attempt to match them against
 * known hashes from configured ABIs
 */
function extractFunctionsAndEvents(contractCode, signatureMatcher) {
    const functionMatches = new Set();
    const eventMatches = new Set();
    const code = Buffer.from(contractCode.slice(2), 'hex');
    for (let i = 0, len = code.length; i < len; i++) {
        const opcode = code[i];
        // Look for PUSH<n> opcodes - their pushData may contain a log topic or function signature hash
        if (opcode >= 0x60 && opcode <= 0x7f) {
            const dataLength = opcode - 0x5f;
            if (dataLength === 32 || dataLength === 4) {
                const data = code.slice(i + 1, i + dataLength + 1).toString('hex');
                const match = signatureMatcher(data);
                if (match) {
                    ;
                    (dataLength === 4 ? functionMatches : eventMatches).add(match);
                }
            }
            i += dataLength;
        }
    }
    const functions = [...functionMatches].sort();
    const events = [...eventMatches].sort();
    return { functions, events };
}
exports.extractFunctionsAndEvents = extractFunctionsAndEvents;
/** Creates a hash of all function and event signatures of a contract used to match ABIs against EVM bytecode */
function computeContractFingerprint({ functions, events } = { functions: [], events: [] }) {
    if (functions.length === 0 && events.length === 0) {
        return;
    }
    const fingerprint = wasm_1.sha3(`${functions.join(',')}|${events.join(',')}`).slice(2);
    return fingerprint;
}
exports.computeContractFingerprint = computeContractFingerprint;
/**
 * Attempts to determine if the given account address is a smart contract and
 * determines some additional information if that's the case
 */
async function getContractInfo(address, resources) {
    const { ethClient, classification, abiRepo } = resources;
    const signatureMatcher = (sig) => abiRepo.getMatchingSignature(sig);
    const contractNameLookup = (address, fingerprint) => { var _a, _b, _c; return (_b = (_a = abiRepo.getContractByAddress(address)) === null || _a === void 0 ? void 0 : _a.contractName) !== null && _b !== void 0 ? _b : (_c = abiRepo.getContractByFingerprint(fingerprint)) === null || _c === void 0 ? void 0 : _c.contractName; };
    debug('Retrieving contract information for address %s', address);
    if (signatureMatcher == null) {
        return { isContract: true };
    }
    const code = await ethClient.request(requests_1.getCode(address));
    if (code === '0x') {
        return { isContract: false };
    }
    const fnsEvts = extractFunctionsAndEvents(code, (fingerprint) => signatureMatcher(fingerprint, address));
    const fingerprint = computeContractFingerprint(fnsEvts);
    if (debug_logging_1.TRACE_ENABLED) {
        trace('Computed fingerprint %s from contract code %O', fingerprint, fnsEvts);
    }
    const contractName = fingerprint != null && contractNameLookup != null ? contractNameLookup(address, fingerprint) : undefined;
    const contractType = await classification.classifyContract({ address, code });
    const properties = await getContractProperties(classification, contractType, address);
    return {
        isContract: true,
        fingerprint,
        contractName,
        contractType,
        properties
    };
}
exports.getContractInfo = getContractInfo;
async function getContractProperties(classification, contractType, address) {
    const isGnosis = contractType.name != null ? contractType.name.includes('Gnosis') : false;
    const isTokenPair = contractType.name === 'TokenPair';
    if (contractType.standards != null || isGnosis || isTokenPair) {
        // const target = (Array.isArray(contractType.proxies) && !isGnosis)
        //     ? contractType.proxies[contractType.proxies.length -1].target
        //     : address;
        const target = address;
        if (target != null) {
            try {
                const properties = await classification.getContractProperties(target, contractType);
                return properties;
            }
            catch (e) {
                classification.contractIgnoreCache.set(target, true);
                warn(`Couldn't get properties %s (%s)`, target, e.message);
            }
        }
    }
    return undefined;
}
async function contractInfo({ address, resources }) {
    const { abiRepo, contractInfoCache } = resources;
    if (abiRepo == null) {
        return;
    }
    const result = await cache_1.cachedAsync(address, contractInfoCache, (addr) => getContractInfo(addr, resources));
    return result;
}
exports.contractInfo = contractInfo;
