"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbiRepository = exports.sortAbis = void 0;
const path_1 = require("path");
const debug_logging_1 = require("@splunkdlt/debug-logging");
const decode_1 = require("./decode");
const files_1 = require("./files");
const signature_1 = require("./signature");
const { warn, debug, info, trace } = debug_logging_1.createModuleDebug('abi:repo');
/** Sort abi defintions in consistent priority order */
function sortAbis(abis) {
    if (abis.length > 1) {
        const sortedAbis = [...abis];
        sortedAbis.sort((a, b) => {
            // Always prefer user-supplied abis with an contract address attached
            const aHasAddress = a.contractAddress != null;
            const bHasAddress = b.contractAddress != null;
            if (aHasAddress !== bHasAddress) {
                return aHasAddress ? -1 : 1;
            }
            // Prefer user-supplied abi
            const aHasFingerprint = a.contractFingerprint != null;
            const bHasFingerprint = b.contractFingerprint != null;
            if (aHasFingerprint !== bHasFingerprint) {
                return aHasFingerprint ? -1 : 1;
            }
            return (
            // Prefer abis with shorter function name as longer ones
            // are sometimes used to deliberately produce signature hash
            // collisions
            a.name.length - b.name.length ||
                // Prefer abis with fewer parameters
                a.inputs.length - b.inputs.length ||
                // Sort the rest by name
                a.name.localeCompare(b.name) ||
                // Last restort is to string compare the full signature
                signature_1.computeSignature(a).localeCompare(signature_1.computeSignature(b)));
        });
        return sortedAbis;
    }
    return abis;
}
exports.sortAbis = sortAbis;
class AbiRepository {
    constructor(config, logging) {
        this.config = config;
        this.logging = logging;
        this.signatures = new Map();
        this.contractsByFingerprint = new Map();
        this.contractsByAddress = new Map();
    }
    addToSignatures(sig, abis) {
        const match = this.signatures.get(sig);
        this.signatures.set(sig, sortAbis(match == null ? abis : [...match, ...abis]));
    }
    async initialize() {
        var _a;
        const config = this.config;
        debug('Initializing ABI repository with config %O', config);
        if ((_a = config.directory) === null || _a === void 0 ? void 0 : _a.length) {
            const abiCount = await this.loadAbisFromDir(config.directory, config);
            info('Loaded %d ABIs from directory %s', abiCount, config.directory);
        }
        if (config.decodeAnonymous) {
            const fnCount = await this.loadAnonymousSignatures(path_1.join(__dirname, '../../data/fns.abisigs.gz'));
            const evCount = await this.loadAnonymousSignatures(path_1.join(__dirname, '../../data/evts.abisigs.gz'));
            info('Loaded %d anonymous ABI signatures from built-in signature files', fnCount + evCount);
        }
    }
    async loadAnonymousSignatures(file) {
        debug('Loading anonymous signatures from %s', file);
        let count = 0;
        const { entries } = await files_1.loadSignatureFile(file);
        for (const [sig, abis] of entries) {
            this.addToSignatures(sig, abis);
            count++;
        }
        return count;
    }
    async loadAbisFromDir(dir, config) {
        let count = 0;
        for await (const abiFile of files_1.searchAbiFiles(dir, config)) {
            await this.loadAbiFile(abiFile, config);
            count++;
        }
        return count;
    }
    async loadAbiFile(path, config) {
        await files_1.loadAbiFile(path, config)
            .then(abiFileContents => {
            this.addAbi(abiFileContents);
        })
            .catch(e => {
            warn('failed to load abi %s', e);
        });
    }
    addAbi(abiFileContents) {
        const contractInfo = {
            contractName: abiFileContents.contractName,
            fileName: abiFileContents.fileName
        };
        if (abiFileContents.contractAddress != null) {
            this.contractsByAddress.set(abiFileContents.contractAddress.toLowerCase(), contractInfo);
        }
        if (abiFileContents.contractFingerprint != null) {
            if (this.contractsByFingerprint.has(abiFileContents.contractFingerprint)) {
                warn('Duplicate contract fingerprint for contracts %o and %o', contractInfo, this.contractsByFingerprint.get(abiFileContents.contractFingerprint));
            }
            else {
                this.contractsByFingerprint.set(abiFileContents.contractFingerprint, contractInfo);
            }
        }
        for (const { sig, abi } of abiFileContents.entries) {
            const signatureHash = signature_1.computeSignatureHash(sig, abi.type);
            this.addToSignatures(signatureHash, [abi]);
        }
    }
    get signatureCount() {
        return this.signatures.size;
    }
    getMatchingAbi(signatureHash) {
        const candidates = this.signatures.get(signatureHash);
        return candidates != null ? { anonymous: true, candidates } : undefined;
    }
    getMatchingSignature(signatureHash) {
        var _a;
        const candidates = this.signatures.get(signatureHash);
        trace('getMatchingSignature(%o) --> ', signatureHash, (_a = candidates === null || candidates === void 0 ? void 0 : candidates.filter(c => c.contractFingerprint != null)) === null || _a === void 0 ? void 0 : _a.map(c => signature_1.computeSignature(c)));
        if (candidates != null) {
            // We only want to consider signatures from contracts in our repo
            // that already have a fingerprint
            const filtered = candidates.filter(c => c.contractFingerprint != null);
            const signatures = new Set(filtered.map(c => signature_1.computeSignature(c)));
            if (signatures.size > 1) {
                throw new Error(`Multiple signatures (${[...signatures].join(', ')}) for sig hash ${signatureHash}`);
            }
            if (filtered.length > 0) {
                return signature_1.computeSignature(filtered[0]);
            }
        }
    }
    getContractByFingerprint(fingerprint) {
        return this.contractsByFingerprint.get(fingerprint);
    }
    getContractByAddress(address) {
        return this.contractsByAddress.get(address === null || address === void 0 ? void 0 : address.toLowerCase());
    }
    findMatchingAbis(sigHash, { contractFingerprint, contractAddress }) {
        const match = this.signatures.get(sigHash);
        if (match != null) {
            trace('Looking for contract match %o for signature hash %s; candidates: %O', { contractAddress, contractFingerprint }, sigHash, match.map(item => ({
                name: item.contractName,
                signature: signature_1.computeSignature(item),
                address: item.contractAddress,
                fingerprint: item.contractFingerprint
            })));
            if (contractAddress != null) {
                const addressMatch = match.find(c => c.contractAddress === contractAddress.toLowerCase());
                if (addressMatch != null) {
                    return { candidates: [addressMatch], anonymous: false };
                }
            }
            if (contractFingerprint != null) {
                const fingerprintMatch = match.filter(c => c.contractFingerprint === contractFingerprint);
                if (fingerprintMatch.length > 0) {
                    return { candidates: fingerprintMatch, anonymous: false };
                }
            }
            if (match.length > 0) {
                if (this.config.requireContractMatch === false) {
                    return { candidates: match, anonymous: false };
                }
                return { candidates: match, anonymous: true };
            }
            if (debug_logging_1.TRACE_ENABLED) {
                trace('No matching contract found for method signature hash %s and contract fingerprint %s', sigHash, contractFingerprint);
            }
        }
        return { candidates: [], anonymous: true };
    }
    abiDecode(sigHash, matchParams, decoder) {
        var _a, _b;
        const matchingAbis = this.findMatchingAbis(sigHash, matchParams);
        trace('Found %d matching ABIs for signature %s', (_b = (_a = matchingAbis === null || matchingAbis === void 0 ? void 0 : matchingAbis.candidates) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, sigHash);
        if (matchingAbis != null) {
            if (matchingAbis.anonymous && !this.config.decodeAnonymous) {
                return;
            }
            if (matchingAbis.candidates.length === 0) {
                trace('No matching ABI found for signature hash %s', sigHash);
                return;
            }
            try {
                return decoder(matchingAbis.candidates, matchingAbis.anonymous);
            }
            catch (e) {
                if (matchingAbis.anonymous) {
                    debug('Failed to decode anonymous ABI', e);
                }
                else {
                    if (this.logging.showDecodeWarnings) {
                        warn('Failed to decode ABI');
                    }
                }
            }
        }
    }
    decodeFunctionCall(data, matchParams) {
        const sigHash = data.slice(2, 10);
        return this.abiDecode(sigHash, matchParams, (abi, anon) => decode_1.decodeBestMatchingFunctionCall(data, abi, anon, this.config.reconcileStructShapeFromTuples));
    }
    decodeLogEvent(logEvent, matchParams) {
        const sigHash = logEvent.topics[0].slice(2);
        const { data, topics } = logEvent;
        return this.abiDecode(sigHash, matchParams, (abi, anon) => decode_1.decodeBestMatchingLogEvent(data, topics, abi, anon, this.config.reconcileStructShapeFromTuples));
    }
    async shutdown() {
        this.signatures.clear();
    }
}
exports.AbiRepository = AbiRepository;
