"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.EVMTransactionStorage = exports.EVMTransactionModel = void 0;
const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
const Loggify_1 = require("../../../../decorators/Loggify");
const logger_1 = __importDefault(require("../../../../logger"));
const baseTransaction_1 = require("../../../../models/baseTransaction");
const cache_1 = require("../../../../models/cache");
const events_1 = require("../../../../models/events");
const walletAddress_1 = require("../../../../models/walletAddress");
const config_1 = require("../../../../services/config");
const storage_1 = require("../../../../services/storage");
const utils_1 = require("../../../../utils");
const erc20_1 = require("../abi/erc20");
const erc721_1 = require("../abi/erc721");
const invoice_1 = require("../abi/invoice");
const multisend_1 = require("../abi/multisend");
const multisig_1 = require("../abi/multisig");
function requireUncached(module) {
    delete require.cache[require.resolve(module)];
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    return require(module);
}
const Erc20Decoder = requireUncached('abi-decoder');
Erc20Decoder.addABI(erc20_1.ERC20Abi);
function getErc20Decoder() {
    return Erc20Decoder;
}
const Erc721Decoder = requireUncached('abi-decoder');
Erc721Decoder.addABI(erc721_1.ERC721Abi);
function getErc721Decoder() {
    return Erc721Decoder;
}
const InvoiceDecoder = requireUncached('abi-decoder');
InvoiceDecoder.addABI(invoice_1.InvoiceAbi);
function getInvoiceDecoder() {
    return InvoiceDecoder;
}
const MultisigDecoder = requireUncached('abi-decoder');
MultisigDecoder.addABI(multisig_1.MultisigAbi);
function getMultisigDecoder() {
    return MultisigDecoder;
}
const MultisendDecoder = requireUncached('abi-decoder');
MultisendDecoder.addABI(multisend_1.MultisendAbi);
function getMultisendDecoder() {
    return MultisendDecoder;
}
let EVMTransactionModel = class EVMTransactionModel extends baseTransaction_1.BaseTransaction {
    constructor(storage = storage_1.Storage) {
        super(storage);
    }
    async onConnect() {
        super.onConnect();
        this.collection.createIndex({ chain: 1, network: 1, to: 1 }, { background: true, sparse: true });
        this.collection.createIndex({ chain: 1, network: 1, from: 1 }, { background: true, sparse: true });
        this.collection.createIndex({ chain: 1, network: 1, from: 1, nonce: 1 }, { background: true, sparse: true });
        this.collection.createIndex({ chain: 1, network: 1, 'abiType.params.0.value': 1, blockTimeNormalized: 1 }, {
            background: true,
            partialFilterExpression: { 'abiType.type': 'ERC20', 'abiType.name': 'transfer' }
        });
        this.collection.createIndex({ chain: 1, network: 1, 'calls.abiType.params.value': 1, blockTimeNormalized: 1 }, {
            background: true,
            partialFilterExpression: { 'calls.abiType.type': 'ERC20', 'calls.abiType.params.type': 'address' }
        });
        this.collection.createIndex({ chain: 1, network: 1, 'internal.action.to': 1 }, {
            background: true,
            sparse: true
        });
        this.collection.createIndex({ chain: 1, network: 1, 'calls.to': 1 }, {
            background: true,
            sparse: true
        });
        this.collection.createIndex({ chain: 1, network: 1, 'effects.to': 1, blockTimeNormalized: 1 }, {
            background: true,
            partialFilterExpression: { 'effects.to': { $exists: true } }
        });
        this.collection.createIndex({ chain: 1, network: 1, 'effects.from': 1, blockTimeNormalized: 1 }, {
            background: true,
            partialFilterExpression: { 'effects.from': { $exists: true } }
        });
    }
    async batchImport(params) {
        const operations = [];
        operations.push(this.pruneMempool({ ...params }));
        const txOps = await this.addTransactions({ ...params });
        logger_1.default.debug('Writing Transactions: %o', txOps.length);
        operations.push(...(0, utils_1.partition)(txOps, txOps.length / config_1.Config.get().maxPoolSize).map(txBatch => this.collection.bulkWrite(txBatch.map(op => this.toMempoolSafeUpsert(op, params.height)), { ordered: false })));
        await Promise.all(operations);
        if (params.initialSyncComplete) {
            await this.expireBalanceCache(txOps);
        }
        // Create events for mempool txs
        if (params.height < 0 /* SpentHeightIndicators.minimum */) {
            for (const op of txOps) {
                const filter = op.updateOne.filter;
                const tx = { ...op.updateOne.update.$set, ...filter };
                await events_1.EventStorage.signalTx(tx);
                await events_1.EventStorage.signalAddressCoin({
                    address: tx.to,
                    coin: { value: Number(tx.value), address: tx.to, chain: params.chain, network: params.network, mintTxid: tx.txid }
                });
            }
        }
    }
    getAllTouchedAddresses(tx) {
        const { to, from, effects } = tx;
        const toBatch = new Set();
        const fromBatch = new Set();
        const addToBatch = (batch, obj) => {
            // Adds string representation to batch to guard uniqueness since {} != {} but '{}' == '{}'
            batch.add(JSON.stringify(obj));
        };
        addToBatch(toBatch, { address: to });
        addToBatch(fromBatch, { address: from });
        if (effects && effects.length) {
            for (const effect of effects) {
                // Handle internal value transfers
                if (!effect.contractAddress) {
                    addToBatch(toBatch, { address: effect.to });
                    addToBatch(fromBatch, { address: effect.from });
                }
                else if (effect.type == 'ERC20:transfer') {
                    // Handle ERC20s
                    addToBatch(toBatch, { address: effect.to, tokenAddress: effect.contractAddress });
                    addToBatch(fromBatch, { address: effect.from, tokenAddress: effect.contractAddress });
                }
            }
        }
        // Convert Set made up of unique strings back to object representations
        const tos = Array.from(toBatch).map(strObj => JSON.parse(strObj));
        const froms = Array.from(fromBatch).map(strObj => JSON.parse(strObj));
        return { tos, froms };
    }
    async expireBalanceCache(txOps) {
        for (const op of txOps) {
            const { chain, network } = op.updateOne.filter;
            const { tos, froms } = this.getAllTouchedAddresses(op.updateOne.update.$set);
            const uniqueBatch = tos.concat(froms);
            for (const payload of uniqueBatch) {
                const lowerAddress = payload.address.toLowerCase();
                const cacheKey = payload.tokenAddress
                    ? `getBalanceForAddress-${chain}-${network}-${lowerAddress}-${payload.tokenAddress.toLowerCase()}`
                    : `getBalanceForAddress-${chain}-${network}-${lowerAddress}`;
                await cache_1.CacheStorage.expire(cacheKey);
            }
        }
    }
    async addTransactions(params) {
        const { blockTimeNormalized, chain, height, network, parentChain, forkHeight } = params;
        if (parentChain && forkHeight && height < forkHeight) {
            const parentTxs = await exports.EVMTransactionStorage.collection
                .find({ blockHeight: height, chain: parentChain, network })
                .toArray();
            return parentTxs.map(parentTx => {
                return {
                    updateOne: {
                        filter: { txid: parentTx.txid, chain, network },
                        update: {
                            $set: {
                                ...parentTx,
                                wallets: new Array()
                            }
                        },
                        upsert: true,
                        forceServerObjectId: true
                    }
                };
            });
        }
        else {
            return Promise.all(
            // Get all "to" and "from" addresses so we can add the any corresponding wallets
            params.txs.map(async (tx) => {
                const { tos, froms } = this.getAllTouchedAddresses(tx);
                const toAddresses = tos.map(a => a.address);
                const fromAddresses = froms.map(a => a.address);
                const walletsAddys = await walletAddress_1.WalletAddressStorage.collection
                    .find({ chain, network, address: { $in: [...fromAddresses, ...toAddresses] } })
                    .toArray();
                const wallets = (0, utils_1.uniqBy)(walletsAddys.map(w => w.wallet), w => w.toHexString());
                // If config value is set then only store needed tx properties
                let leanTx = tx;
                if (config_1.Config.chainConfig({ chain, network }).leanTransactionStorage) {
                    leanTx = exports.EVMTransactionStorage.toLeanTransaction(tx);
                }
                return {
                    updateOne: {
                        filter: { txid: tx.txid, chain, network },
                        update: {
                            $set: {
                                ...leanTx,
                                blockTimeNormalized,
                                wallets
                            }
                        },
                        upsert: true,
                        forceServerObjectId: true
                    }
                };
            }));
        }
    }
    async pruneMempool(params) {
        const { chain, network, initialSyncComplete, txs } = params;
        if (!initialSyncComplete) {
            return;
        }
        for (const tx of txs) {
            await this.collection.update({
                chain,
                network,
                from: tx.from,
                nonce: tx.nonce,
                txid: { $ne: tx.txid },
                blockHeight: -1 /* SpentHeightIndicators.pending */
            }, { $set: { blockHeight: -3 /* SpentHeightIndicators.conflicting */, replacedByTxid: tx.txid } }, { w: 0, j: false, multi: true });
        }
        return;
    }
    getTransactions(params) {
        const originalQuery = params.query;
        const { query, options } = storage_1.Storage.getFindOptions(this, params.options);
        const finalQuery = Object.assign({}, originalQuery, query);
        return this.collection.find(finalQuery, options).addCursorFlag('noCursorTimeout', true);
    }
    abiDecode(input) {
        try {
            const erc20Data = getErc20Decoder().decodeMethod(input);
            if (erc20Data) {
                return {
                    type: 'ERC20',
                    ...erc20Data
                };
            }
        }
        catch { /* ignore error */ }
        try {
            const erc721Data = getErc721Decoder().decodeMethod(input);
            if (erc721Data) {
                return {
                    type: 'ERC721',
                    ...erc721Data
                };
            }
        }
        catch { /* ignore error */ }
        try {
            const invoiceData = getInvoiceDecoder().decodeMethod(input);
            if (invoiceData) {
                return {
                    type: 'INVOICE',
                    ...invoiceData
                };
            }
        }
        catch { /* ignore error */ }
        try {
            const multisendData = getMultisendDecoder().decodeMethod(input);
            if (multisendData) {
                return {
                    type: 'MUTLISEND',
                    ...multisendData
                };
            }
        }
        catch { /* ignore error */ }
        try {
            const multisigData = getMultisigDecoder().decodeMethod(input);
            if (multisigData) {
                return {
                    type: 'MULTISIG',
                    ...multisigData
                };
            }
        }
        catch { /* ignore error */ }
        return undefined;
    }
    /**
     * Creates an object with param names as keys instead of an array of objects
     * @param abi
     * @returns object of abi param values that can be accessed with the name as a key
     */
    parseAbiParams(abi) {
        const params = abi.params;
        const parsed = {};
        for (const param of params) {
            const { value } = param;
            parsed[param.name] = value;
        }
        return parsed;
    }
    /**
     * Adds effects details object to in process txs
     */
    addEffectsToTxs(txs) {
        for (const tx of txs) {
            tx.effects = this.getEffects(tx);
        }
    }
    /**
     * Creates an array of all effects for a given tx
     * @param tx A tx object that contains extra data that we don't want to store long term
     * @returns An array of all effects for the transaction
     */
    getEffects(tx) {
        const effects = [];
        try {
            if (tx.calls?.length) { // Geth trace calls[]
                for (const call of tx.calls) {
                    if (call.value && BigInt(call.value) > 0) {
                        // Handle native asset transfer
                        const effect = this._getEffectForNativeTransfer(BigInt(call.value).toString(), call.to, call.from, call.depth);
                        effects.push(effect);
                    }
                    if (call.abiType) { // If there was a known ABI (ERC20, Invoice) transfer within the tx execution
                        // Handle Abi related effects
                        let effect;
                        if (call.type === 'DELEGATECALL') { // Delegate calls are proxy calls within a smart contract
                            // find parent call that's one level up. E.g. if depth = '0_1_2', then find '0_1'
                            const parent = tx.calls.find(c => c.depth === call.depth.split('_').slice(0, -1).join('_')) || { to: tx.to, from: tx.from, input: null }; // Fallback to tx.to and tx.from if no parent found
                            if (parent?.to === call.from && parent?.input === call.input) {
                                // If parent is the same as the current call, then it's just a proxy call
                                continue;
                            }
                            effect = this._getEffectForAbiType(call.abiType, parent.to, parent.from, call.depth);
                        }
                        else {
                            effect = this._getEffectForAbiType(call.abiType, call.to, call.from, call.depth);
                        }
                        if (effect) {
                            effects.push(effect);
                        }
                    }
                }
            }
            else if (tx.internal?.length) { // LEGACY: Used for converting old OpenEthereum/Parity db entries with internal[]
                for (const internalTx of tx.internal) {
                    if (internalTx.action.value && BigInt(internalTx.action.value) > 0) {
                        // Handle native asset transfer
                        const effect = this._getEffectForNativeTransfer(BigInt(internalTx.action.value).toString(), internalTx.action.to, internalTx.action.from || tx.from, internalTx.traceAddress.join('_'));
                        effects.push(effect);
                    }
                    if (internalTx.abiType) {
                        // Handle Abi related effects
                        const effect = this._getEffectForAbiType(internalTx.abiType, internalTx.action.to, internalTx.action.from || tx.from, internalTx.traceAddress.join('_'));
                        if (effect) {
                            effects.push(effect);
                        }
                    }
                }
            }
            else if (tx.abiType) { // We recognized upstream that this is a known ABI tx
                // Handle Abi related effects
                const effect = this._getEffectForAbiType(tx.abiType, tx.to, tx.from, '');
                if (effect) {
                    effects.push(effect);
                }
            }
        }
        catch (err) {
            logger_1.default.error('Error Getting Effects For TxId: %o ::%o', tx.txid, err);
        }
        return effects;
    }
    /**
     * Creates an array of effects that are filtered for relevance to a given list of addresses
     * @param {IEVMTransactionInProcess} tx
     * @param {Array<string>} addresses
     */
    getEffectsForAddresses(tx, addresses) {
        const effects = tx.effects?.length ? tx.effects : this.getEffects(tx);
        const addySet = new Set(addresses.map(a => a.toLowerCase()));
        return effects.filter(effect => addySet.has(effect.to.toLowerCase()) || addySet.has(effect.from.toLowerCase()));
    }
    _getEffectForAbiType(abi, to, from, callStack) {
        // Check that the params are valid before parsing
        if (!to || !from)
            return;
        if (`${abi.type}:${abi.name}` == 'ERC20:transfer') {
            const params = this.parseAbiParams(abi);
            const { _to, _value } = params;
            // Check that the params are valid before parsing
            if (!_to || !_value)
                return;
            return {
                type: 'ERC20:transfer',
                to: crypto_wallet_core_1.Web3.utils.toChecksumAddress(_to),
                from: crypto_wallet_core_1.Web3.utils.toChecksumAddress(from),
                amount: crypto_wallet_core_1.Web3.utils.fromWei(_value, 'wei'),
                contractAddress: crypto_wallet_core_1.Web3.utils.toChecksumAddress(to),
                callStack
            };
        }
        else if (`${abi.type}:${abi.name}` == 'ERC20:transferFrom') {
            const params = this.parseAbiParams(abi);
            const { _to, _from, _value } = params;
            // Check that the params are valid before parsing
            if (!_to || !_from || !_value)
                return;
            return {
                type: 'ERC20:transfer',
                to: crypto_wallet_core_1.Web3.utils.toChecksumAddress(_to),
                from: crypto_wallet_core_1.Web3.utils.toChecksumAddress(_from),
                amount: crypto_wallet_core_1.Web3.utils.fromWei(_value, 'wei'),
                contractAddress: crypto_wallet_core_1.Web3.utils.toChecksumAddress(to),
                callStack
            };
        }
        else if (`${abi.type}:${abi.name}` == 'MULTISIG:submitTransaction') {
            const params = this.parseAbiParams(abi);
            const { destination, value } = params;
            // Check that the params are valid before parsing
            if (!destination || !value)
                return;
            return {
                type: 'MULTISIG:submitTransaction',
                to: crypto_wallet_core_1.Web3.utils.toChecksumAddress(destination),
                from: crypto_wallet_core_1.Web3.utils.toChecksumAddress(from),
                amount: crypto_wallet_core_1.Web3.utils.fromWei(value, 'wei'),
                contractAddress: crypto_wallet_core_1.Web3.utils.toChecksumAddress(to),
                callStack
            };
        }
        else if (`${abi.type}:${abi.name}` == 'MULTISIG:confirmTransaction') {
            return {
                type: 'MULTISIG:confirmTransaction',
                to: '0x0',
                from: crypto_wallet_core_1.Web3.utils.toChecksumAddress(from),
                amount: '0',
                contractAddress: crypto_wallet_core_1.Web3.utils.toChecksumAddress(to),
                callStack
            };
        }
        return;
    }
    _getEffectForNativeTransfer(value, to, from, callStack) {
        const effect = {
            to: crypto_wallet_core_1.Web3.utils.toChecksumAddress(to),
            from: crypto_wallet_core_1.Web3.utils.toChecksumAddress(from),
            amount: crypto_wallet_core_1.Web3.utils.fromWei(value, 'wei'),
            callStack
        };
        return effect;
    }
    /**
     * Receives any type of TX and returns a lean version without unused properties
     * @param tx - transaction to leanify
     */
    toLeanTransaction(tx) {
        const removableProperties = ['data', 'internal', 'calls', 'abiType'];
        for (const prop of removableProperties) {
            if (tx[prop]) {
                delete tx[prop];
            }
        }
        return tx;
    }
    convertRawTx(chain, network, tx, block) {
        if (!block) {
            const txid = tx.hash || '';
            const to = tx.to ? crypto_wallet_core_1.Web3.utils.toChecksumAddress(tx.to) : '';
            const from = tx.from ? crypto_wallet_core_1.Web3.utils.toChecksumAddress(tx.from) : '';
            const value = BigInt(tx.value);
            const gas = BigInt(tx.gas || -1); // -1 indicates unknown
            const gasPrice = BigInt(tx.gasPrice || -1);
            const fee = gas < 0n || gasPrice < 0n ? -1n : gas * gasPrice;
            const abiType = this.abiDecode(tx.input);
            const nonce = BigInt(tx.nonce || 0);
            const convertedTx = {
                chain,
                network,
                blockHeight: Number((0, utils_1.valueOrDefault)(tx.blockNumber, -1)),
                blockHash: (0, utils_1.valueOrDefault)(tx.blockHash, undefined),
                data: Buffer.from(tx.input || '0x'),
                txid,
                blockTime: new Date(),
                blockTimeNormalized: new Date(),
                fee: Number(fee),
                transactionIndex: Number(tx.transactionIndex || 0),
                value: Number(value),
                wallets: [],
                to,
                from,
                gasLimit: Number(gas),
                gasPrice: Number(gasPrice),
                nonce: Number(nonce),
                internal: [],
                calls: []
            };
            if (abiType) {
                convertedTx.abiType = abiType;
            }
            return convertedTx;
        }
        else {
            const { hash: blockHash, time: blockTime, timeNormalized: blockTimeNormalized, height } = block;
            const noBlockTx = this.convertRawTx(chain, network, tx);
            return {
                ...noBlockTx,
                blockHeight: height,
                blockHash,
                blockTime,
                blockTimeNormalized
            };
        }
    }
    // Correct tx.data.toString() => 0xa9059cbb00000000000000000000000001503dfc5ad81bf630d83697e98601871bb211b60000000000000000000000000000000000000000000000000000000000002710
    // Incorrect: tx.data.toString('hex') => 307861393035396362623030303030303030303030303030303030303030303030303031353033646663356164383162663633306438333639376539383630313837316262323131623630303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303032373130
    _apiTransform(tx, options) {
        let transaction = {
            txid: tx.txid || '',
            network: tx.network || '',
            chain: tx.chain || '',
            blockHeight: (0, utils_1.valueOrDefault)(tx.blockHeight, -1),
            blockHash: tx.blockHash || '',
            blockTime: tx.blockTime ? tx.blockTime.toISOString() : '',
            blockTimeNormalized: tx.blockTimeNormalized ? tx.blockTimeNormalized.toISOString() : '',
            fee: (0, utils_1.valueOrDefault)(tx.fee, -1),
            value: (0, utils_1.valueOrDefault)(tx.value, -1),
            gasLimit: (0, utils_1.valueOrDefault)(tx.gasLimit, -1),
            gasPrice: (0, utils_1.valueOrDefault)(tx.gasPrice, -1),
            nonce: (0, utils_1.valueOrDefault)(tx.nonce, 0),
            to: tx.to || '',
            from: tx.from || '',
            effects: tx.effects || []
        };
        // Add non-lean properties if we aren't excluding them
        const config = config_1.Config.chainConfig({ chain: tx.chain, network: tx.network });
        if (config && !config.leanTransactionStorage) {
            const dataStr = tx.data ? tx.data.toString() : '';
            const decodedData = this.abiDecode(dataStr);
            const nonLeanProperties = {
                data: dataStr,
                abiType: tx.abiType || (0, utils_1.valueOrDefault)(decodedData, undefined),
                internal: tx.internal
                    ? tx.internal.map(t => ({ ...t, decodedData: this.abiDecode(t?.action?.input || '0x') }))
                    : [],
                calls: tx.calls ? tx.calls.map(t => ({ ...t, decodedData: this.abiDecode(t.input || '0x') })) : []
            };
            transaction = Object.assign(transaction, nonLeanProperties);
        }
        if (options && options.object) {
            return transaction;
        }
        return JSON.stringify(transaction, crypto_wallet_core_1.Utils.BI.JSONStringifyBigIntReplacer);
    }
};
exports.EVMTransactionModel = EVMTransactionModel;
exports.EVMTransactionModel = EVMTransactionModel = __decorate([
    Loggify_1.LoggifyClass
], EVMTransactionModel);
exports.EVMTransactionStorage = new EVMTransactionModel();
//# sourceMappingURL=transaction.js.map