"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MoralisStateProvider = void 0;
const os_1 = __importDefault(require("os"));
const lru_cache_1 = require("lru-cache");
const request_1 = __importDefault(require("request"));
const config_1 = __importDefault(require("../../../config"));
const logger_1 = __importDefault(require("../../../logger"));
const cache_1 = require("../../../models/cache");
const walletAddress_1 = require("../../../models/walletAddress");
const csp_1 = require("../../../providers/chain-state/evm/api/csp");
const block_1 = require("../../../providers/chain-state/evm/models/block");
const transaction_1 = require("../../../providers/chain-state/evm/models/transaction");
const moralis_utils_1 = require("../../../providers/chain-state/external/adapters/moralis-utils");
const apiStream_1 = require("../../../providers/chain-state/external/streams/apiStream");
const utils_1 = require("../../../utils");
const utils_2 = require("../../../utils");
const streamWithEventPipe_1 = require("../../../utils/streamWithEventPipe");
class MoralisStateProvider extends csp_1.BaseEVMStateProvider {
    constructor(chain) {
        super(chain);
        this.baseUrl = 'https://deep-index.moralis.io/api/v2.2';
        this.baseStreamUrl = 'https://api.moralis-streams.com/streams/evm';
        this.apiKey = config_1.default.externalProviders?.moralis?.apiKey;
        this.baseWebhookurl = config_1.default.externalProviders?.moralis?.webhookBaseUrl;
        this.headers = {
            'Content-Type': 'application/json',
            'X-API-Key': this.apiKey,
        };
        this.blockAtTimeCache = {};
    }
    // @override
    async getBlockBeforeTime(params) {
        const { chain, network, time } = params;
        const date = new Date(time || Date.now());
        const chainNetwork = (0, utils_2.normalizeChainNetwork)(chain, network);
        if (!this.blockAtTimeCache[chainNetwork]) {
            this.blockAtTimeCache[chainNetwork] = new lru_cache_1.LRUCache({ max: 1000 });
        }
        const cachedBlock = this.blockAtTimeCache[chainNetwork].get(date.toISOString());
        if (cachedBlock !== undefined) {
            return cachedBlock;
        }
        const chainId = await this.getChainId({ network });
        const blockNum = await this._getBlockNumberByDate({ chainId, date });
        if (!blockNum) {
            return null;
        }
        const blockId = blockNum.toString();
        const blocks = await this._getBlocks({ chain, network, blockId, args: { limit: 1 } });
        const block = blocks.blocks[0] || null;
        this.blockAtTimeCache[chainNetwork].set(date.toISOString(), block);
        return block;
    }
    // @override
    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) {
            cacheKey += `-type${txType}`;
        }
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            const { rpc } = await this.getWeb3(network, { type: 'historical' });
            const feerate = await rpc.estimateFee({ nBlocks: target, txType });
            return { feerate: Number(feerate), blocks: target };
        }, cache_1.CacheStorage.Times.Minute);
    }
    // @override
    async getLocalTip({ chain, network }) {
        const { web3 } = await this.getWeb3(network);
        const block = await web3.eth.getBlock('latest');
        return block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
    }
    // @override
    async streamBlocks(params) {
        const { chain, network, req, res } = params;
        const { web3 } = await this.getWeb3(network);
        const chainId = await this.getChainId({ network });
        const blockRange = await this.getBlocksRange({ ...params, chainId });
        const tipHeight = Number(await web3.eth.getBlockNumber());
        let isReading = false;
        const stream = new streamWithEventPipe_1.ReadableWithEventPipe({
            objectMode: true,
            async read() {
                if (isReading) {
                    return;
                }
                isReading = true;
                let block;
                let nextBlock;
                try {
                    for (const blockNum of blockRange) {
                        // stage next block in new var so `nextBlock` doesn't get overwritten if needed for `block`
                        const thisNextBlock = parseInt(block?.number) === blockNum + 1 ? block : await web3.eth.getBlock(blockNum + 1);
                        block = parseInt(nextBlock?.number) === blockNum ? nextBlock : await web3.eth.getBlock(blockNum);
                        if (!block) {
                            continue;
                        }
                        nextBlock = thisNextBlock;
                        const convertedBlock = block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
                        convertedBlock.nextBlockHash = nextBlock?.hash;
                        convertedBlock.confirmations = tipHeight - block.number + 1;
                        this.push(convertedBlock);
                    }
                }
                catch (e) {
                    logger_1.default.error('Error streaming blocks: %o', e);
                }
                this.push(null);
            }
        });
        return apiStream_1.ExternalApiStream.onStream(stream, req, res);
    }
    // @override
    async _getTransaction(params) {
        let { network } = params;
        const { chain, txId } = params;
        network = network.toLowerCase();
        const { web3 } = await this.getWeb3(network, { type: 'historical' });
        const tipHeight = Number(await web3.eth.getBlockNumber());
        const chainId = await this.getChainId({ network });
        const found = await this._getTransactionFromMoralis({ chain, network, chainId, txId });
        return { tipHeight, found };
    }
    // @override
    async _buildAddressTransactionsStream(params) {
        const { req, res, args, network, address } = params;
        const chainId = await this.getChainId({ network });
        const txStream = await this._streamAddressTransactionsFromMoralis({
            chainId,
            chain: this.chain,
            network,
            address,
            args: {
                limit: 10, // default limit when querying by address
                ...args
            }
        });
        // TODO unify `ExternalApiStream.onStream` and `Storage.apiStream` which are effectively doing the same thing
        const result = await apiStream_1.ExternalApiStream.onStream(txStream, req, res);
        if (!result?.success) {
            logger_1.default.error('Error mid-stream (streamAddressTransactions): %o', result.error?.log || result.error);
        }
    }
    // @override
    async _buildWalletTransactionsStream(params, streamParams) {
        const { network, args } = params;
        let { transactionStream } = streamParams;
        const { walletAddresses } = streamParams;
        const chainId = await this.getChainId({ network });
        for (const address of walletAddresses) {
            const txStream = await this._streamAddressTransactionsFromMoralis({
                chainId,
                chain: this.chain,
                network,
                address,
                args: {
                    limit: args.limit, // no default limit when querying by wallet. Note: BWS caches txs
                    order: 'ASC',
                    ...args
                }
            });
            transactionStream = txStream.eventPipe(transactionStream);
            // Do not await these promises. They are not critical to the stream.
            walletAddress_1.WalletAddressStorage.updateLastQueryTime({ chain: this.chain, network, address })
                .catch(e => logger_1.default.warn(`Failed to update ${this.chain}:${network} address lastQueryTime: %o`, e)),
                this._addAddressToSubscription({ chainId, address })
                    .catch(e => logger_1.default.warn(`Failed to add address to ${this.chain}:${network} Moralis subscription: %o`, e));
        }
        return transactionStream;
    }
    // @override
    async _getBlocks(params) {
        const { chain, network } = params;
        const blocks = [];
        const { web3 } = await this.getWeb3(network);
        const chainId = await this.getChainId({ network });
        const blockRange = await this.getBlocksRange({ ...params, chainId });
        for (const blockNum of blockRange) {
            const block = await web3.eth.getBlock(blockNum);
            const nextBlock = await web3.eth.getBlock(block.number + 1n);
            const convertedBlock = block_1.EVMBlockStorage.convertRawBlock(chain, network, block);
            convertedBlock.nextBlockHash = nextBlock?.hash;
            blocks.push(convertedBlock);
        }
        const tipHeight = Number(await web3.eth.getBlockNumber());
        return { tipHeight, blocks };
    }
    // @override
    async _getBlockNumberByDate(params) {
        const { date, chainId } = params;
        if (!date || !(0, utils_1.isDateValid)(date)) {
            throw new Error('Invalid date');
        }
        if (!chainId) {
            throw new Error('Invalid chainId');
        }
        const query = (0, moralis_utils_1.transformMoralisQueryParams)({ chainId, args: { date } });
        const queryStr = (0, moralis_utils_1.buildMoralisQueryString)(query);
        return new Promise((resolve, reject) => {
            (0, request_1.default)({
                method: 'GET',
                url: `${this.baseUrl}/dateToBlock${queryStr}`,
                headers: this.headers,
                json: true
            }, (err, _data, body) => {
                if (err) {
                    return reject(err);
                }
                return resolve(body.block);
            });
        });
    }
    /** MORALIS METHODS */
    async _getTransactionFromMoralis(params) {
        const { chain, network, chainId, txId } = params;
        const query = (0, moralis_utils_1.buildMoralisQueryString)({ chain: (0, moralis_utils_1.formatMoralisChainId)(chainId), include: 'internal_transactions' });
        return new Promise((resolve, reject) => {
            (0, request_1.default)({
                method: 'GET',
                url: `${this.baseUrl}/transaction/${txId}${query}`,
                headers: this.headers,
                json: true
            }, (err, data) => {
                if (err) {
                    return reject(err);
                }
                if (typeof data === 'string') {
                    return reject(new Error(data));
                }
                const tx = data.body;
                if (tx.message === 'No transaction found') {
                    return resolve(null);
                }
                return resolve((0, moralis_utils_1.transformMoralisTransaction)({ chain, network, ...tx }));
            });
        });
    }
    _streamAddressTransactionsFromMoralis(params) {
        const { chainId, chain, network, address, args } = params;
        if (args.tokenAddress) {
            return this._streamERC20TransactionsByAddress({ chainId, chain, network, address, tokenAddress: args.tokenAddress, args });
        }
        if (!address) {
            throw new Error('Missing address');
        }
        if (!chainId) {
            throw new Error('Invalid chainId');
        }
        const query = (0, moralis_utils_1.transformMoralisQueryParams)({ chainId, args }); // throws if no chain or network
        const queryStr = (0, moralis_utils_1.buildMoralisQueryString)({
            ...query,
            order: args.order ?? query.order ?? 'DESC', // preserve direction-derived order, default to descending
            limit: args.pageSize || 10, // limit per request/page. total limit (args.limit) is checked in apiStream._read()
            include: 'internal_transactions'
        });
        args.transform = (tx) => {
            const _tx = (0, moralis_utils_1.transformMoralisTransaction)({ chain, network, ...tx });
            const confirmations = this._calculateConfirmations(_tx, args.tipHeight);
            return transaction_1.EVMTransactionStorage._apiTransform({ ..._tx, confirmations }, { object: true });
        };
        return new apiStream_1.ExternalApiStream(`${this.baseUrl}/${address}${queryStr}`, this.headers, args);
    }
    _streamERC20TransactionsByAddress({ chainId, chain, network, address, tokenAddress, args }) {
        if (!address) {
            throw new Error('Missing address');
        }
        if (!tokenAddress) {
            throw new Error('Missing token address');
        }
        if (!chainId) {
            throw new Error('Invalid chainId');
        }
        const queryTransform = (0, moralis_utils_1.transformMoralisQueryParams)({ chainId, args }); // throws if no chain or network
        const queryStr = (0, moralis_utils_1.buildMoralisQueryString)({
            ...queryTransform,
            order: args.order ?? queryTransform.order ?? 'DESC', // preserve direction-derived order, default to descending
            limit: args.pageSize || 10, // limit per request/page. total limit (args.limit) is checked in apiStream._read()
            contract_addresses: [tokenAddress],
        });
        args.transform = (tx) => {
            const _tx = (0, moralis_utils_1.transformMoralisTokenTransfer)({ chain, network, ...tx });
            const confirmations = this._calculateConfirmations(_tx, args.tipHeight);
            return transaction_1.EVMTransactionStorage._apiTransform({ ..._tx, confirmations }, { object: true });
        };
        return new apiStream_1.ExternalApiStream(`${this.baseUrl}/${address}/erc20/transfers${queryStr}`, this.headers, args);
    }
    _calculateConfirmations(tx, tip) {
        let confirmations = 0;
        if (tx.blockHeight && tx.blockHeight >= 0) {
            confirmations = tip - tx.blockHeight + 1;
        }
        return confirmations;
    }
    /**
     * Request wrapper for moralis Streams (subscriptions)
     * @param method
     * @param url
     * @param body
     * @returns
     */
    _subsRequest(method, url, body) {
        return new Promise((resolve, reject) => {
            (0, request_1.default)({
                method,
                url,
                headers: this.headers,
                json: true,
                body
            }, (err, data) => {
                if (err) {
                    logger_1.default.error(`Error with Moralis subscription call ${method}:${url}: ${err.stack || err.message || err}`);
                    return reject(err);
                }
                if (typeof data === 'string') {
                    logger_1.default.warn(`Moralis subscription ${method}:${url} returned a string: ${data}`);
                    return reject(new Error(data));
                }
                return resolve(data.body);
            });
        });
    }
    async createAddressSubscription(params) {
        const { chain, network, chainId } = params;
        const _chainId = (0, moralis_utils_1.formatMoralisChainId)(chainId);
        const result = await this._subsRequest('PUT', this.baseStreamUrl, {
            description: `Bitcore ${_chainId} - ${os_1.default.hostname()} - addresses`,
            // tag: '',
            chainIds: [_chainId],
            webhookUrl: `${this.baseWebhookurl}/${chain}/${network}/moralis`,
            includeNativeTxs: true,
            includeInternalTxs: true
        });
        if (!result.id) {
            throw new Error('Failed to create subscription: ' + JSON.stringify(result));
        }
        return result;
    }
    async getAddressSubscriptions() {
        const subs = await this._subsRequest('GET', this.baseStreamUrl + '?limit=100');
        return subs.result;
    }
    deleteAddressSubscription(params) {
        const { sub } = params;
        return this._subsRequest('DELETE', `${this.baseStreamUrl}/${sub.id}`);
    }
    async updateAddressSubscription(params) {
        const { sub, addressesToAdd, addressesToRemove, status } = params;
        let moralisSub = null;
        if (addressesToAdd && addressesToAdd.length > 0) {
            moralisSub = await this._subsRequest('POST', `${this.baseStreamUrl}/${sub.id}/address`, { address: addressesToAdd });
        }
        else if (addressesToRemove && addressesToRemove.length > 0) {
            moralisSub = await this._subsRequest('DELETE', `${this.baseStreamUrl}/${sub.id}/address`, { address: addressesToRemove });
        }
        else if (status) {
            moralisSub = await this._subsRequest('POST', `${this.baseStreamUrl}/${sub.id}/status`, { status });
        }
        if (moralisSub?.message) {
            throw new Error(moralisSub.message);
        }
        return moralisSub?.id ? moralisSub : sub; // fallback to sub in case there's nothing to update (e.g. addressesToAdd is an empty array)
    }
    webhookToCoinEvents(params) {
        const { chain, network, webhook } = params;
        if (webhook.body.confirmed) {
            // Moralis broadcasts both confirmed and unconfirmed.
            // Filtering out confirmed de-duplicates events.
            return [];
        }
        const coinEvents = webhook.body.txs.flatMap(tx => this._transformWebhookTransaction({ chain, network, tx, webhook: webhook.body }));
        return coinEvents;
    }
    _transformWebhookTransaction(params) {
        const { chain, network, tx } = params;
        const events = [];
        for (const address of tx.triggered_by) {
            events.push({
                address,
                coin: {
                    chain,
                    network,
                    value: tx.value,
                    address: tx.toAddress,
                    mintTxid: tx.hash
                }
            });
        }
        return events;
    }
    async _addAddressToSubscription({ chainId, address }) {
        const subs = await this.getAddressSubscriptions();
        const sub = subs?.find(sub => sub.chainIds.includes('0x' + chainId.toString(16)));
        if (sub) {
            await this.updateAddressSubscription({ sub, addressesToAdd: [address] });
        }
    }
}
exports.MoralisStateProvider = MoralisStateProvider;
//# sourceMappingURL=csp.js.map