"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseSVMStateProvider = void 0;
const crypto_rpc_1 = require("@bitpay-labs/crypto-rpc");
const transaction_parser_1 = require("@bitpay-labs/crypto-rpc/lib/sol/transaction-parser");
const mpl_token_metadata_1 = require("@metaplex-foundation/mpl-token-metadata");
const umi_bundle_defaults_1 = require("@metaplex-foundation/umi-bundle-defaults");
const spl_token_registry_1 = require("@solana/spl-token-registry");
const lru_cache_1 = require("lru-cache");
const logger_1 = __importDefault(require("../../../../logger"));
const cache_1 = require("../../../../models/cache");
const config_1 = require("../../../../services/config");
const utils_1 = require("../../../../utils");
const streamWithEventPipe_1 = require("../../../../utils/streamWithEventPipe");
const apiStream_1 = require("../../external/streams/apiStream");
const internal_1 = require("../../internal/internal");
;
class BaseSVMStateProvider extends internal_1.InternalStateProvider {
    constructor(chain = 'SOL') {
        super(chain);
        this.chain = chain;
        this.blockAtTimeCache = {};
        this.config = config_1.Config.get().chains[this.chain];
        BaseSVMStateProvider.initializeRpcs(this.chain);
    }
    static initializeRpcs(chain) {
        if (BaseSVMStateProvider.rpcInitialized[chain]) {
            return;
        }
        const configs = config_1.Config.get().chains[chain];
        for (const [network, config] of Object.entries(configs)) {
            const chainNetwork = (0, utils_1.normalizeChainNetwork)(chain, network);
            BaseSVMStateProvider.rpcs[chainNetwork] = { historical: [], realtime: [] };
            BaseSVMStateProvider.rpcIndicies[chainNetwork] = { historical: 0, realtime: 0 };
            const providers = config.provider ? [config.provider] : config.providers || [];
            for (const providerConfig of providers) {
                const wsPort = providerConfig.wsPort ?? providerConfig.port;
                const rpcConfig = { ...providerConfig, chain, wsPort };
                const rpc = new crypto_rpc_1.CryptoRpc(rpcConfig).get(chain);
                const { protocol, host, port } = providerConfig;
                const umi = (0, umi_bundle_defaults_1.createUmi)(`${protocol}://${host}${port ? `:${port}` : ''}`)
                    .use((0, mpl_token_metadata_1.mplTokenMetadata)());
                const rpcObj = {
                    rpc,
                    connection: rpc.rpc,
                    umi,
                    dataType: rpcConfig.dataType || 'combined',
                };
                if (rpcObj.dataType === 'historical' || rpcObj.dataType === 'combined') {
                    BaseSVMStateProvider.rpcs[chainNetwork].historical.push(rpcObj);
                }
                if (rpcObj.dataType === 'realtime' || rpcObj.dataType === 'combined') {
                    BaseSVMStateProvider.rpcs[chainNetwork].realtime.push(rpcObj);
                }
            }
        }
        BaseSVMStateProvider.rpcInitialized[chain] = true;
    }
    static teardownRpcs() {
        for (const [chainNetwork, rpcObj] of Object.entries(BaseSVMStateProvider.rpcs)) {
            logger_1.default.info('Tearing down RPC connections for %o', chainNetwork);
            for (const rpc of rpcObj.historical.concat(rpcObj.realtime)) {
                try {
                    rpc.connection?.disconnect?.();
                }
                catch { /* ignore -- already disconnected or non-socket connection (e.g. http) */ }
            }
            delete BaseSVMStateProvider.rpcs[chainNetwork];
        }
    }
    async getRpc(network, params) {
        const chainNetwork = (0, utils_1.normalizeChainNetwork)(this.chain, network);
        const type = params?.type || 'realtime';
        if (BaseSVMStateProvider.rpcs[chainNetwork][type].length === 1) {
            return BaseSVMStateProvider.rpcs[chainNetwork][type][0];
        }
        // Load-balance the RPCs in a round-robin fashion
        const lastUsedIndex = BaseSVMStateProvider.rpcIndicies[chainNetwork][type];
        const getNextIndex = (index) => (index + 1) % BaseSVMStateProvider.rpcs[chainNetwork][type].length;
        const initialIndex = getNextIndex(lastUsedIndex);
        let index = initialIndex;
        let rpc;
        do {
            rpc = BaseSVMStateProvider.rpcs[chainNetwork][type][index];
            try {
                if (Date.now() - (rpc.lastPingTime || 0) < 10000) { // Keep the rpc from being blasted with ping calls
                    return rpc;
                }
                await Promise.race([
                    rpc.connection.getSlot({ commitment: 'confirmed' }).send(),
                    new Promise((_, reject) => setTimeout(reject, 5000))
                ]);
                rpc.lastPingTime = Date.now();
                // Update the most recently used index
                BaseSVMStateProvider.rpcIndicies[chainNetwork][type] = index;
                return rpc;
            }
            catch {
                logger_1.default.warn('Unresponsive RPC detected for %o:%o, trying next RPC', this.chain, network);
            }
            index = getNextIndex(index);
        } while (index !== initialIndex);
        // If none have worked, return the last (successful?) rpc
        logger_1.default.warn('All %o:%o RPCs are unresponsive, returning last used RPC', this.chain, network);
        return BaseSVMStateProvider.rpcs[chainNetwork][type][lastUsedIndex];
    }
    async getFee(params) {
        const { network, target = 4, rawTx, signatures } = params;
        let cacheKey = `getFee-${this.chain}-${network}-${target}`;
        cacheKey += rawTx ? `-${rawTx}` : '';
        cacheKey += signatures ? `-${signatures}` : '';
        return cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            let feerate;
            const { rpc, connection } = await this.getRpc(network);
            try {
                if (rawTx) {
                    feerate = await rpc.estimateFee({ nBlocks: target, rawTx });
                }
                else {
                    const { height } = await rpc.getTip();
                    const { transactions } = await connection.getBlock(height);
                    const _signatures = signatures || 1;
                    let lamportsPerSig = 5000; // default
                    if (transactions?.length) {
                        const fee = transactions[0]?.meta?.fee || 5000;
                        const numberOfSignatures = transactions[0]?.transactions?.signatures?.length || 1;
                        lamportsPerSig = fee / numberOfSignatures;
                    }
                    // Total Fee = Number of Signatures × Lamports per Signature
                    feerate = _signatures * lamportsPerSig;
                }
            }
            catch (err) {
                logger_1.default.error('getFee: %o', err.stack || err.message || err);
                throw err;
            }
            return { feerate, blocks: target };
        }, cache_1.CacheStorage.Times.Minute);
    }
    async getTransaction(params) {
        const { txId, network } = params;
        const { rpc } = await this.getRpc(network);
        const tx = await rpc.getTransaction({ txid: txId });
        if (!tx)
            return undefined;
        return this.txTransform(network, { tx });
    }
    async streamTransactions(params) {
        return new Promise(async (resolve, reject) => {
            try {
                const { chain, network, req, res, args } = params;
                let { blockHeight } = args;
                const { limit = 50 } = args;
                if (!chain || !network) {
                    throw new Error('Missing chain or network');
                }
                if (blockHeight !== undefined) {
                    blockHeight = Number(blockHeight);
                }
                else {
                    throw new Error('Missing required block height / slot.');
                }
                const { rpc } = await this.getRpc(network);
                const block = await rpc.getBlock({ height: blockHeight, transactionDetails: 'signatures' });
                if (!block) {
                    throw new Error('Block not found: ' + blockHeight);
                }
                const stream = new streamWithEventPipe_1.TransformWithEventPipe({
                    objectMode: true,
                    passThrough: true
                });
                let count = 0;
                for (const signature of block?.signatures || []) {
                    if (limit && count >= limit)
                        break;
                    const transformedTx = await this._getTransformedTx(rpc, network, { signature });
                    stream.push(transformedTx);
                    count++;
                }
                stream.push(null);
                const result = await apiStream_1.ExternalApiStream.onStream(stream, req, res);
                if (!result?.success) {
                    logger_1.default.error('Error mid-stream (streamTransactions): %o', result.error?.log || result.error);
                }
                return resolve();
            }
            catch (err) {
                logger_1.default.error('Error streaming block transactions: %o', err.stack || err.message || err);
                reject(err);
            }
        });
    }
    async streamAddressTransactions(params) {
        return new Promise(async (resolve, reject) => {
            const { req, res } = params;
            try {
                const addressStream = await this._buildAddressTransactionsStream(params);
                const result = await apiStream_1.ExternalApiStream.onStream(addressStream, req, res, { jsonl: true });
                if (!result?.success) {
                    logger_1.default.error('Error mid-stream (streamAddressTransactions): %o', result.error?.log || result.error);
                }
                return resolve();
            }
            catch (err) {
                return reject(err);
            }
        });
    }
    async streamWalletTransactions(params) {
        return new Promise(async (resolve, reject) => {
            try {
                const { wallet, req, res } = params;
                const walletStream = new streamWithEventPipe_1.TransformWithEventPipe({ objectMode: true, passThrough: true });
                const walletAddresses = (await this.getWalletAddresses(wallet._id)).map(waddress => waddress.address);
                const addressStreams = [];
                for (const address of walletAddresses) {
                    const addressStream = await this._buildAddressTransactionsStream({ ...params, address });
                    addressStreams.push(addressStream);
                }
                apiStream_1.ExternalApiStream.mergeStreams(addressStreams, walletStream);
                const result = await apiStream_1.ExternalApiStream.onStream(walletStream, req, res, { jsonl: true });
                if (!result?.success) {
                    logger_1.default.error('Error mid-stream (streamWalletTransactions): %o', result.error?.log || result.error);
                }
                return resolve();
            }
            catch (err) {
                logger_1.default.error('Error streaming wallet transactions: %o', err.stack || err.message || err);
                return reject(err);
            }
        });
    }
    async _buildAddressTransactionsStream(params) {
        const { args, network, address } = params;
        const { limit = 50 } = args;
        const tokenAddress = args?.tokenAddress || args?.mintAddress;
        const addressStream = new streamWithEventPipe_1.TransformWithEventPipe({
            objectMode: true,
            passThrough: true,
            read() { } // no-op; we’ll push manually
        });
        (async () => {
            try {
                const { rpc, connection } = await this.getRpc(network);
                let before;
                let count = 0;
                let _address = address;
                if (tokenAddress) {
                    try {
                        const { rpc } = await this.getRpc(network);
                        _address = await rpc.getConfirmedAta({ solAddress: address, mintAddress: tokenAddress });
                        if (!_address)
                            throw new Error('Missing ATA');
                    }
                    catch (e) {
                        const errMsg = 'Error getting ATA address';
                        logger_1.default.error(`${errMsg} %o`, e.stack || e.message || e);
                        throw new Error(errMsg);
                    }
                }
                do {
                    // fetch the next page of signatures
                    const txlimit = Math.min(100, limit);
                    const txList = await connection.getSignaturesForAddress(_address, { limit: txlimit, before }).send();
                    if (!txList.length)
                        break;
                    before = txList[txList.length - 1].signature;
                    for (const tx of txList.reverse()) {
                        if (limit && count >= limit)
                            break;
                        const transformedTx = await this._getTransformedTx(rpc, network, tx, _address, tokenAddress);
                        if (transformedTx) {
                            addressStream.push(JSON.stringify(transformedTx) + '\n');
                            count++;
                            if (transformedTx.category === 'send' || transformedTx.category === 'move') {
                                addressStream.push(JSON.stringify({
                                    id: transformedTx.txid,
                                    txid: transformedTx.txid,
                                    category: 'fee',
                                    satoshis: -1 * transformedTx.fee, // lamports
                                    height: transformedTx.height,
                                    blockTime: transformedTx.blockTime
                                }) + '\n');
                            }
                        }
                    }
                } while (!limit || count < limit);
                addressStream.push(null);
            }
            catch (err) {
                addressStream.emit('error', err);
            }
        })().catch(err => {
            logger_1.default.error('Unhandled error in _buildAddressTransactionsStream:', err);
            addressStream.emit('error', err);
        });
        return addressStream;
    }
    async _getTransformedTx(rpc, network, tx, address, tokenAddress) {
        try {
            const parsedTx = await rpc.getTransaction({ txid: tx.signature });
            return this.txTransform(network, { txStatuses: tx, tx: parsedTx, targetAddress: address, tokenAddress });
        }
        catch (err) {
            return {
                error: err?.message,
                txid: tx.signature,
                network,
                chain: 'SOL',
                status: tx?.confirmationStatus,
                height: Number(tx.slot)
            };
        }
    }
    txTransform(network, params) {
        const { block, tx, txStatus, targetAddress, tokenAddress } = params;
        let blockTime;
        let blockHash;
        if (block) {
            ({ blockHeight: blockTime, blockTime, blockhash: blockHash } = block);
        }
        blockTime = blockTime || tx?.blockTime;
        const { feePayerAddress, slot, meta, version, txid } = tx;
        const recentBlockhash = tx.lifetimeConstraint.blockhash || blockHash;
        const date = new Date((blockTime || 0) * 1000);
        const status = tx.status || txStatus?.confirmationStatus;
        const error = meta?.err || txStatus?.err;
        const transactionError = error ? { error: JSON.stringify(error, (_, v) => typeof v === 'bigint' ? v.toString() : v) } : null;
        const txType = version;
        const instructions = tx.instructions;
        const fee = meta?.fee;
        const from = feePayerAddress;
        const target = targetAddress || from;
        const recipientAddresses = new Set();
        let mainToAddress = null;
        let value = 0;
        if (instructions?.[transaction_parser_1.instructionKeys.TRANSFER_SOL]?.length > 0) {
            const solTransfers = instructions[transaction_parser_1.instructionKeys.TRANSFER_SOL];
            mainToAddress = solTransfers.find(transfer => transfer.destination !== from)?.destination || null;
            for (const transfer of solTransfers) {
                if (transfer.destination !== from) {
                    recipientAddresses.add(transfer.destination);
                }
            }
            ;
            if (!tokenAddress) {
                const relevantTransfers = targetAddress ? solTransfers.filter(transfer => [transfer.destination, transfer.source].includes(targetAddress)) : solTransfers;
                value = relevantTransfers.reduce((sum, transfer) => sum + Number(transfer.amount), 0);
            }
        }
        const transferTokenInstructions = instructions?.[transaction_parser_1.instructionKeys.TRANSFER_TOKEN] || [];
        const transferCheckedTokenInstructions = instructions?.[transaction_parser_1.instructionKeys.TRANSFER_CHECKED_TOKEN] || [];
        const allTokenTransfers = [...transferTokenInstructions, ...transferCheckedTokenInstructions];
        if (allTokenTransfers.length > 0) {
            const filteredTransferCheckedToken = tokenAddress ? transferCheckedTokenInstructions.filter(transfer => tokenAddress.toLowerCase() === transfer.mint.toLowerCase()) : transferCheckedTokenInstructions;
            const tokenTransfers = [...transferTokenInstructions, ...filteredTransferCheckedToken];
            if (!tokenTransfers?.length) {
                return;
            }
            if (tokenAddress || !mainToAddress) {
                mainToAddress = tokenTransfers.find(transfer => transfer.destination !== from)?.destination || null;
            }
            for (const transfer of tokenTransfers) {
                if (transfer.destination !== from) {
                    recipientAddresses.add(transfer.destination);
                }
            }
            ;
            if (tokenAddress) {
                const relevantTransfers = targetAddress ? tokenTransfers.filter(transfer => [transfer.destination, transfer.source].includes(targetAddress)) : tokenTransfers;
                value = relevantTransfers.reduce((sum, transfer) => sum + Number(transfer.amount), 0);
            }
        }
        const allRecipients = Array.from(recipientAddresses);
        let txCategory = 'other';
        if (instructions?.[transaction_parser_1.instructionKeys.TRANSFER_SOL]?.length > 0 ||
            allTokenTransfers.length > 0) {
            if (allRecipients.length === 0) {
                txCategory = 'move';
            }
            else {
                const solSentOut = instructions?.[transaction_parser_1.instructionKeys.TRANSFER_SOL]?.some(transfer => transfer.source === target && transfer.destination !== target) || false;
                const tokensSentOut = allTokenTransfers.some(transfer => transfer.source === target && transfer.destination !== target) || false;
                const solReceived = instructions?.[transaction_parser_1.instructionKeys.TRANSFER_SOL]?.some(transfer => transfer.destination === target && transfer.source !== target) || false;
                const tokensReceived = allTokenTransfers.some(transfer => transfer.destination === target && transfer.source !== target) || false;
                if ((solSentOut || tokensSentOut) && !(solReceived || tokensReceived)) {
                    txCategory = 'send';
                    value = value * -1;
                }
                else if (!(solSentOut || tokensSentOut) && (solReceived || tokensReceived)) {
                    txCategory = 'receive';
                }
                else if ((solSentOut || tokensSentOut) && (solReceived || tokensReceived)) {
                    // Both sending and receiving in the same transaction
                    txCategory = 'move';
                }
            }
        }
        return {
            txid,
            fee: Number(fee),
            height: slot,
            from,
            initialFrom: from,
            txType,
            address: mainToAddress, // This is the main "to" address
            recipients: allRecipients, // New field for all recipient addresses
            blockTime: date,
            error: transactionError,
            network,
            chain: 'SOL',
            status,
            recentBlockhash,
            instructions,
            satoshis: value,
            category: txCategory
        };
    }
    async getWalletBalanceAtTime(params) {
        const { network, time } = params;
        if (time) {
            if (params.args) {
                params.args.time = time;
            }
            else {
                params.args = { time };
            }
        }
        if (params.wallet._id === undefined) {
            throw new Error('Wallet balance can only be retrieved for wallets with the _id property');
        }
        const addresses = await this.getWalletAddresses(params.wallet._id);
        const addressBalancePromises = addresses.map(({ address }) => this.getBalanceForAddress({ chain: this.chain, network, address, args: params.args }));
        const addressBalances = await Promise.all(addressBalancePromises);
        const balance = addressBalances.reduce((prev, cur) => ({
            unconfirmed: BigInt(prev.unconfirmed) + BigInt(cur.unconfirmed),
            confirmed: BigInt(prev.confirmed) + BigInt(cur.confirmed),
            balance: BigInt(prev.balance) + BigInt(cur.balance)
        }), { unconfirmed: 0n, confirmed: 0n, balance: 0n });
        return {
            unconfirmed: Number(balance.unconfirmed),
            confirmed: Number(balance.confirmed),
            balance: Number(balance.balance)
        };
    }
    async getBalanceForAddress(params) {
        const { chain, address, network, args } = params;
        const { rpc, connection } = await this.getRpc(network);
        const tokenAddress = args?.tokenAddress || args?.mintAddress;
        if (args?.time) {
            const block = await this.getBlockBeforeTime({ chain, network, time: args.time });
            if (!block) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            const sigInBlock = block.transactions?.[0];
            if (!sigInBlock) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            let ata;
            if (tokenAddress) {
                ata = await rpc.getConfirmedAta({ solAddress: address, mintAddress: tokenAddress });
                if (!ata) {
                    throw new Error('Missing ATA');
                }
            }
            const txList = await connection.getSignaturesForAddress(ata || address, { limit: 1, before: sigInBlock }).send();
            if (!txList.length || !txList[0]?.signature) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            const tx = await rpc.getTransaction({ txid: txList[0].signature });
            if (!tx) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            const index = tx.accountKeys?.findIndex(acct => acct === (ata || address));
            if (index === undefined || index === -1) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            let balance;
            if (tokenAddress) {
                const tokenBalance = tx.meta?.postTokenBalances?.find(tb => tb.accountIndex === index && tb.mint.toLowerCase() === tokenAddress.toLowerCase());
                const decimals = tokenBalance?.uiTokenAmount?.decimals;
                const [left, right = ''] = (tokenBalance?.uiTokenAmount?.uiAmount || 0).toString().split('.');
                balance = Number(left + right.padEnd(decimals, '0'));
            }
            else {
                balance = tx.meta?.postBalances?.[index];
            }
            if ((!balance && balance !== 0) || isNaN(Number(balance))) {
                throw new Error(`Balance not found at ${args.time}`);
            }
            return { confirmed: Number(balance), unconfirmed: 0, balance: Number(balance) };
        }
        const cacheKey = tokenAddress
            ? `getBalanceForAddress-SOL-${network}-${address}-${tokenAddress.toLowerCase()}`
            : `getBalanceForAddress-SOL-${network}-${address}`;
        return await cache_1.CacheStorage.getGlobalOrRefresh(cacheKey, async () => {
            if (tokenAddress) {
                try {
                    const ata = await rpc.getConfirmedAta({ solAddress: address, mintAddress: tokenAddress });
                    const { value } = await connection.getTokenAccountBalance(ata).send();
                    const balance = value?.amount || 0;
                    return { confirmed: balance, unconfirmed: 0, balance };
                }
                catch (e) {
                    if (e?.message?.includes('ATA not initialized')) {
                        return { confirmed: 0, unconfirmed: 0, balance: 0 };
                    }
                    throw e;
                }
            }
            else {
                const balance = await rpc.getBalance({ address });
                return { confirmed: balance, unconfirmed: 0, balance };
            }
        }, cache_1.CacheStorage.Times.Minute);
    }
    async getBlock(params) {
        const { height, blockId, network } = params;
        const { rpc } = await this.getRpc(network);
        const slot = Number(height || blockId);
        const block = await rpc.getBlock({ height: slot });
        return this.blockTransform(network, block, slot);
    }
    async getBlockBeforeTime(params) {
        const { network, time = new Date() } = params;
        const date = new Date(time || Date.now());
        const { rpc } = await this.getRpc(network);
        const chainNetwork = (0, utils_1.normalizeChainNetwork)(this.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;
        }
        let beforeTimeSlot = await this._findSlotByDate(network, date);
        if (!beforeTimeSlot) {
            return null;
        }
        let block;
        for (let attempt = 0; attempt < 10; attempt++) { // retry in case of skipped or missing slots
            try {
                block = await rpc.getBlock({ height: beforeTimeSlot });
                break;
            }
            catch (e) {
                if (e?.message?.includes('missing in long-term storage')) {
                    beforeTimeSlot--;
                    continue;
                }
                throw e;
            }
        }
        if (block) {
            block = this.blockTransform(network, block, beforeTimeSlot);
        }
        this.blockAtTimeCache[chainNetwork].set(date.toISOString(), block);
        return block;
    }
    async streamBlocks(params) {
        return new Promise(async (resolve, reject) => {
            try {
                const { chain, network, req, res } = params;
                if (!chain || !network) {
                    throw new Error('Missing chain or network');
                }
                const { rpc } = await this.getRpc(network);
                const blockRange = await this.getBlocksRange({ ...params });
                const { height } = await rpc.getTip();
                const stream = new streamWithEventPipe_1.TransformWithEventPipe({
                    objectMode: true,
                    passThrough: true
                });
                try {
                    let block;
                    let nextBlock;
                    for (const blockNum of blockRange) {
                        const thisNextBlock = Number(block?.height) === blockNum + 1 ? block : await this._getTransformedBlock(rpc, network, blockNum + 1);
                        block = Number(nextBlock?.number) === blockNum ? nextBlock : await this._getTransformedBlock(rpc, network, blockNum);
                        if (!block) {
                            continue;
                        }
                        nextBlock = thisNextBlock;
                        block.nextBlockHash = nextBlock?.hash;
                        block.confirmations = Number(BigInt(height) - BigInt(block.height) + 1n);
                        stream.push(block);
                    }
                }
                catch (e) {
                    logger_1.default.error('Error streaming blocks: %o', e);
                }
                stream.push(null);
                const result = await apiStream_1.ExternalApiStream.onStream(stream, req, res, { jsonl: true });
                if (!result?.success) {
                    logger_1.default.error('Error mid-stream (streamBlocks): %o', result.error?.log || result.error);
                }
                return resolve();
            }
            catch (err) {
                logger_1.default.error('Error streaming blocks: %o', err.stack || err.message || err);
                reject(err);
            }
        });
    }
    async _getTransformedBlock(rpc, network, height) {
        const block = await rpc.getBlock({ height: Number(height) });
        try {
            return this.blockTransform(network, block, height);
        }
        catch (err) {
            return {
                error: err?.message,
                height,
                network,
                chain: 'SOL',
                status: block?.confirmationStatus,
            };
        }
    }
    blockTransform(network, block, height) {
        return {
            chain: this.chain,
            network,
            height: Number(height),
            hash: block?.blockhash,
            time: new Date(Number(block?.blockTime) * 1000),
            timeNormalized: new Date(Number(block?.blockTime) * 1000),
            previousBlockHash: block?.previousBlockHash,
            transactions: block?.signatures,
            transactionCount: block?.signatures?.length,
            size: block?.transactions?.length,
            reward: Number(block?.rewards[0]?.lamports),
            processed: true,
        };
    }
    async getBlocksRange(params) {
        const { chain, network, sinceBlock, args = {} } = params;
        const { blockId } = params;
        let { startDate, endDate, limit = 10 } = args;
        const { date, sort = { height: -1 } } = args;
        const query = {};
        if (!chain || !network) {
            throw new Error('Missing required chain and/or network param');
        }
        // limit - 1 because startBlock is inclusive; ensure limit is >= 0
        limit = Math.max(limit - 1, 0);
        let height = null;
        if (date) {
            startDate = new Date(date);
            endDate = new Date(date);
            endDate.setDate(endDate.getDate() + 1);
        }
        if (startDate || endDate) {
            if (startDate) {
                query.startBlock = await this._findSlotByDate(network, startDate) || 0;
            }
            if (endDate) {
                query.endBlock = await this._findSlotByDate(network, endDate) || 0;
            }
        }
        // Get range
        if (sinceBlock) {
            const height = Number(sinceBlock);
            if (isNaN(height) || height.toString(10) != sinceBlock) {
                throw new Error('invalid block id provided');
            }
            const { rpc } = await this.getRpc(network);
            const { height: _height } = await rpc.getTip();
            const tipHeight = Number(_height) || 0;
            if (tipHeight < height) {
                return [];
            }
            if (!tipHeight) {
                throw new Error('unable to fetch tip height');
            }
            query.endBlock = query.endBlock ?? tipHeight;
            query.startBlock = query.startBlock ?? query.endBlock - limit;
        }
        else if (blockId) {
            height = Number(blockId);
        }
        if (height != null) {
            query.startBlock = height;
            query.endBlock = height + limit;
        }
        if (query.startBlock == null || query.endBlock == null) {
            // Calaculate range with options
            const { rpc } = await this.getRpc(network);
            const { height: _height } = await rpc.getTip();
            const tipHeight = Number(_height) || 0;
            query.endBlock = query.endBlock ?? tipHeight;
            query.startBlock = query.startBlock ?? query.endBlock - limit;
        }
        if (query.endBlock - query.startBlock > limit) {
            query.endBlock = query.startBlock + limit;
        }
        const r = (0, utils_1.range)(query.startBlock, query.endBlock + 1); // +1 since range is [start, end)
        if (sort?.height === -1 && query.startBlock < query.endBlock) {
            return r.reverse();
        }
        return r;
    }
    async _findSlotByDate(network, targetDate) {
        const { connection } = await this.getRpc(network);
        let lo = await connection.getFirstAvailableBlock().send();
        let hi = await connection.getSlot({ commitment: 'finalized' }).send();
        let result = null;
        const targetTime = Math.floor(targetDate.getTime() / 1000);
        const loBlockTime = await connection.getBlockTime(lo).send();
        if (loBlockTime !== null && loBlockTime >= targetTime) {
            return lo;
        }
        let errorCount = 0;
        let mid = 0n;
        while (lo <= hi) {
            mid = mid || (lo + hi) / 2n;
            let blockTime = null;
            try {
                blockTime = await connection.getBlockTime(mid).send();
                if (errorCount > 0) {
                    errorCount = 0; // reset error count on successful fetch
                }
            }
            catch (e) {
                // possible rate limit exceeded or missing slot
                errorCount++;
                if (errorCount >= 10) {
                    throw new Error(e?.message || 'Too many errors occurred');
                }
                if (e?.message?.includes('missing in long-term storage')) {
                    mid--;
                }
                await new Promise(resolve => setTimeout(resolve, 500));
                continue;
            }
            if (blockTime === null) {
                lo = mid + 1n;
            }
            else if (blockTime < targetTime) {
                lo = mid + 1n;
            }
            else {
                result = mid;
                hi = mid - 1n;
            }
            mid = 0n; // reset mid for next iteration
        }
        return Number(result) || null;
    }
    async getPriorityFee(params) {
        const { percentile, network } = params;
        const { rpc } = await this.getRpc(network);
        const fee = await rpc.estimateMaxPriorityFee({ percentile });
        return fee;
    }
    async broadcastTransaction(params) {
        const { rawTx, network } = params;
        const { rpc } = await this.getRpc(network);
        const txids = new Array();
        const rawTxs = typeof rawTx === 'string' ? [rawTx] : rawTx;
        for (const tx of rawTxs) {
            const txid = await rpc.sendRawTransaction({ rawTx: tx });
            txids.push(txid);
        }
        return txids.length === 1 ? txids[0] : txids;
    }
    async getWalletBalance(params) {
        const { network } = params;
        if (params.wallet._id === undefined) {
            throw new Error('Wallet balance can only be retrieved for wallets with the _id property');
        }
        const addresses = await this.getWalletAddresses(params.wallet._id);
        const addressBalancePromises = addresses.map(({ address }) => this.getBalanceForAddress({ chain: this.chain, network, address, args: params.args }));
        const addressBalances = await Promise.all(addressBalancePromises);
        const balance = addressBalances.reduce((prev, cur) => ({
            unconfirmed: BigInt(prev.unconfirmed) + BigInt(cur.unconfirmed),
            confirmed: BigInt(prev.confirmed) + BigInt(cur.confirmed),
            balance: BigInt(prev.balance) + BigInt(cur.balance)
        }), { unconfirmed: 0n, confirmed: 0n, balance: 0n });
        return {
            unconfirmed: Number(balance.unconfirmed),
            confirmed: Number(balance.confirmed),
            balance: Number(balance.balance)
        };
        ;
    }
    async getRentExemptionAmount(params) {
        const { space, network } = params;
        const { connection } = await this.getRpc(network);
        return await connection.getMinimumBalanceForRentExemption(Number(space)).send();
    }
    async getCoinsForTx(_params) {
        return {
            inputs: [],
            outputs: []
        };
    }
    async getTokenAccountAddresses(params) {
        const { network, address } = params;
        const { rpc, connection } = await this.getRpc(network);
        const addresses = await rpc.getTokenAccountsByOwner({ address });
        const result = [];
        for (const addr of addresses) {
            if (addr.state === 'initialized') {
                const { value } = await connection.getTokenAccountBalance(addr.pubkey).send();
                result.push({ mintAddress: addr.mint, ataAddress: addr.pubkey, decimals: value.decimals });
            }
        }
        return result;
    }
    async getSPLTokenInfo(network, tokenAddress) {
        const TOKEN_PROGRAM_ADDRESS = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
        const TOKEN_2022_ADDR = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
        const { umi, connection } = await this.getRpc(network);
        let decimals;
        let programType;
        let programAddress;
        let name = '';
        let symbol = '';
        try {
            let error;
            let token;
            try {
                token = await (0, mpl_token_metadata_1.fetchDigitalAsset)(umi, tokenAddress);
            }
            catch (e) {
                error = e;
            }
            if (token) {
                name = token.metadata.name;
                symbol = token.metadata.symbol;
                decimals = token.mint.decimals;
            }
            else {
                // If a token doesn't use the Token Metadata Standard (above), it uses the Solana Labs Token List (below). 
                // This list is obsolete since June 20,2022
                const provider = await new spl_token_registry_1.TokenListProvider().resolve();
                const networkId = {
                    mainnet: 101,
                    testnet: 102,
                    devnet: 103
                };
                const tokenList = provider.filterByChainId(networkId[network]).getList();
                const tokenMap = tokenList.reduce((map, item) => {
                    map.set(item.address, item);
                    return map;
                }, new Map());
                token = tokenMap.get(tokenAddress);
                name = token?.name;
                symbol = token?.symbol;
                decimals = token?.decimals;
                if (!token && error) {
                    throw error;
                }
            }
        }
        catch (err) {
            logger_1.default.error('Error getting SPL token info: %o', err);
        }
        try {
            const result = await connection.getAccountInfo(tokenAddress).send();
            const owner = result?.value?.owner?.toString?.();
            if (!owner) {
                throw new Error(`Mint account not found or unreadable: ${tokenAddress}`);
            }
            if (owner === TOKEN_PROGRAM_ADDRESS) {
                programType = 'token';
                programAddress = TOKEN_PROGRAM_ADDRESS;
            }
            else if (owner === TOKEN_2022_ADDR) {
                programType = 'token2022';
                programAddress = TOKEN_2022_ADDR;
            }
            if (!programAddress) {
                logger_1.default.warn(`Unknown token program owner for ${tokenAddress}: ${owner}`);
            }
        }
        catch (err) {
            logger_1.default.error('Error getting SPL token program info: %o', err);
        }
        return { name, symbol, decimals, programType, programAddress };
    }
    async getLocalTip(params) {
        const { network } = params;
        const { rpc, connection } = await this.getRpc(network);
        const height = await connection.getSlot({ commitment: 'confirmed' }).send();
        const block = await rpc.getBlock({ height });
        return this.blockTransform(network, block, height);
    }
    validateRawTx(rawTx) {
        const MAX_TRANSACTION_SIZE = 1232; // Solana's packet size limit
        const MIN_TRANSACTION_SIZE = 64;
        if (!rawTx) {
            throw new Error('Missing raw transaction string');
        }
        if (!(Buffer.from(rawTx, 'base64').toString('base64') === rawTx)) {
            throw new Error('Invalid base64 encoding');
        }
        const buffer = Buffer.from(rawTx, 'base64');
        if (buffer.length > MAX_TRANSACTION_SIZE) {
            throw new Error(`Transaction size (${buffer.length}) exceeds maximum (${MAX_TRANSACTION_SIZE})`);
        }
        if (buffer.length < MIN_TRANSACTION_SIZE) {
            throw new Error('Transaction too small to be valid');
        }
        return true;
    }
    async decodeRawTransaction(params) {
        const { network, rawTx } = params;
        const { rpc } = await this.getRpc(network);
        const decodedTx = await rpc.decodeRawTransaction({ rawTx });
        return decodedTx ? JSON.parse(JSON.stringify(decodedTx, (_, v) => typeof v === 'bigint' ? v.toString() : v)) : null;
    }
}
exports.BaseSVMStateProvider = BaseSVMStateProvider;
BaseSVMStateProvider.rpcs = {};
BaseSVMStateProvider.rpcIndicies = {};
BaseSVMStateProvider.rpcInitialized = {};
//# sourceMappingURL=csp.js.map