"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CacheProvider = void 0;
const lodash_1 = require("lodash");
const constants_1 = require("./constants");
const rateLimitedProvider_1 = require("./rateLimitedProvider");
const utils_1 = require("./utils");
class CacheProvider extends rateLimitedProvider_1.RateLimitedProvider {
    redisClient;
    standardTtlBlockDistance;
    noTtlBlockDistance;
    providerCacheTtl;
    getBlockByNumberPrefix;
    getLogsCachePrefix;
    callCachePrefix;
    getTransactionReceiptPrefix;
    baseTTL;
    constructor(providerCacheNamespace, redisClient, standardTtlBlockDistance = Number.POSITIVE_INFINITY, noTtlBlockDistance = Number.POSITIVE_INFINITY, providerCacheTtl = constants_1.PROVIDER_CACHE_TTL, ...jsonRpcConstructorParams) {
        super(...jsonRpcConstructorParams);
        this.redisClient = redisClient;
        this.standardTtlBlockDistance = standardTtlBlockDistance;
        this.noTtlBlockDistance = noTtlBlockDistance;
        this.providerCacheTtl = providerCacheTtl;
        const { chainId } = this.network;
        const cachePrefix = `${providerCacheNamespace},${new URL(this.connection.url).hostname},${chainId}`;
        this.getBlockByNumberPrefix = `${cachePrefix}:getBlockByNumber,`;
        this.getLogsCachePrefix = `${cachePrefix}:eth_getLogs,`;
        this.callCachePrefix = `${cachePrefix}:eth_call,`;
        this.getTransactionReceiptPrefix = `${cachePrefix}:eth_getTransactionReceipt,`;
        const _ttlVar = providerCacheTtl;
        const _ttl = Number(_ttlVar);
        if (isNaN(_ttl) || _ttl <= 0) {
            throw new Error(`PROVIDER_CACHE_TTL (${_ttlVar}) must be numeric and > 0`);
        }
        this.baseTTL = _ttl;
    }
    async send(method, params) {
        let cacheType = this.redisClient ? await this.cacheType(method, params) : utils_1.CacheType.NONE;
        if (cacheType === utils_1.CacheType.NONE) {
            return await super.send(method, params);
        }
        const redisKey = this.buildRedisKey(method, params);
        const redisResult = await this.redisClient?.get(redisKey);
        if (redisResult) {
            return JSON.parse(redisResult);
        }
        const result = await super.send(method, params);
        if (method === "eth_getTransactionReceipt" && result == null) {
            return result;
        }
        if (cacheType === utils_1.CacheType.DECIDE_TTL_POST_SEND) {
            const blockNumber = this.getBlockNumberFromRpcResponse(method, result);
            cacheType = await this.cacheTypeForBlock(blockNumber);
            if (cacheType === utils_1.CacheType.NONE) {
                return result;
            }
        }
        switch (cacheType) {
            case utils_1.CacheType.WITH_TTL:
                {
                    const ttl = this.baseTTL + Math.ceil((0, lodash_1.random)(-constants_1.PROVIDER_CACHE_TTL_MODIFIER, constants_1.PROVIDER_CACHE_TTL_MODIFIER, true) * this.baseTTL);
                    await this.redisClient?.set(redisKey, JSON.stringify(result), ttl);
                }
                break;
            case utils_1.CacheType.NO_TTL:
                await this.redisClient?.set(redisKey, JSON.stringify(result), Number.POSITIVE_INFINITY);
                break;
            default:
                throw new Error(`Unexpected Cache type: ${cacheType}`);
        }
        return result;
    }
    buildRedisKey(method, params) {
        switch (method) {
            case "eth_getBlockByNumber":
                return this.getBlockByNumberPrefix + JSON.stringify(params);
            case "eth_getLogs":
                return this.getLogsCachePrefix + JSON.stringify(params);
            case "eth_call":
                return this.callCachePrefix + JSON.stringify(params);
            case "eth_getTransactionReceipt":
                return this.getTransactionReceiptPrefix + JSON.stringify(params);
            default:
                throw new Error(`CacheProvider::buildRedisKey: invalid JSON-RPC method ${method}`);
        }
    }
    cacheType(method, params) {
        if (method === "eth_getLogs") {
            const [{ fromBlock, toBlock }] = params;
            const fromBlockNumber = parseInt(String(fromBlock), 16);
            const toBlockNumber = parseInt(String(toBlock), 16);
            if (Number.isNaN(fromBlockNumber) || Number.isNaN(toBlockNumber)) {
                return Promise.resolve(utils_1.CacheType.NONE);
            }
            if (toBlockNumber < fromBlockNumber) {
                throw new Error("CacheProvider::shouldCache toBlock cannot be smaller than fromBlock.");
            }
            return this.cacheTypeForBlock(toBlock);
        }
        else if ("eth_call" === method || "eth_getBlockByNumber" === method) {
            const idx = method === "eth_getBlockByNumber" ? 0 : 1;
            const blockNumber = parseInt(String(params[idx]), 16);
            if (Number.isNaN(blockNumber)) {
                return Promise.resolve(utils_1.CacheType.NONE);
            }
            return this.cacheTypeForBlock(blockNumber);
        }
        else if ("eth_getTransactionReceipt" === method) {
            return Promise.resolve(utils_1.CacheType.DECIDE_TTL_POST_SEND);
        }
        else {
            return Promise.resolve(utils_1.CacheType.NONE);
        }
    }
    getBlockNumberFromRpcResponse(method, result) {
        if (method === "eth_getTransactionReceipt") {
            const receipt = result;
            return Number(receipt.blockNumber);
        }
        else {
            throw new Error(`CacheProvider::getBlockNumberFromRpcResponse: unsupported JSON-RPC method ${method}`);
        }
    }
    async cacheTypeForBlock(blockNumber) {
        const currentBlockNumber = await super._getInternalBlockNumber(constants_1.BLOCK_NUMBER_TTL * 1000);
        const headDistance = currentBlockNumber - blockNumber;
        if (headDistance > this.noTtlBlockDistance) {
            return utils_1.CacheType.NO_TTL;
        }
        if (headDistance > this.standardTtlBlockDistance) {
            return utils_1.CacheType.WITH_TTL;
        }
        return utils_1.CacheType.NONE;
    }
}
exports.CacheProvider = CacheProvider;
//# sourceMappingURL=cachedProvider.js.map