"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureValidBigNumber = ensureValidBigNumber;
exports.isValidArbitrageStrategy = isValidArbitrageStrategy;
exports.validateBalances = validateBalances;
exports.isValidAddress = isValidAddress;
exports.isSupportedAssetNumberT3rn = isSupportedAssetNumberT3rn;
exports.isOrderIdValid = isOrderIdValid;
exports.canBeValidBigNumber = canBeValidBigNumber;
exports.checkAssetBalance = checkAssetBalance;
exports.getAssetsForNetwork = getAssetsForNetwork;
exports.getAssetsForNetworkMoralis = getAssetsForNetworkMoralis;
exports.getAssetsForNetworkPyth = getAssetsForNetworkPyth;
exports.getAssetAddressByNumber = getAssetAddressByNumber;
exports.getAssetNumberByAddress = getAssetNumberByAddress;
exports.getAssetAddressByName = getAssetAddressByName;
exports.getAssetNameByAddress = getAssetNameByAddress;
exports.getCachedOrders = getCachedOrders;
exports.setCachedOrders = setCachedOrders;
exports.crawlPrice = crawlPrice;
exports.omit = omit;
const tslib_1 = require("tslib");
const ethers_1 = require("ethers");
const types_1 = require("./types");
const logger_1 = require("../utils/logger");
const enums_1 = require("./enums");
const assetAddresses_1 = require("./assetAddresses");
const assetAddressesMoralis_1 = require("./assetAddressesMoralis");
const helpers_1 = require("../utils/helpers");
const axios_1 = tslib_1.__importDefault(require("axios"));
const assetAddressesPyth_1 = require("./assetAddressesPyth");
function ensureValidBigNumber(value) {
    // Check if the value is a BigNumber
    if (!(typeof value === 'object' && value.type === 'BigNumber')) {
        return null;
    }
    try {
        return ethers_1.BigNumber.from(value);
    }
    catch {
        return null;
    }
}
// minProfitPerOrder: BigNumber // in target asset
// minProfitRate: number // in %
// maxAmountPerOrder: BigNumber // in target asset
// minAmountPerOrder: BigNumber // in target asset
// maxShareOfMyBalancePerOrder: number // in %
function isValidArbitrageStrategy(item) {
    // Verify the fields of the OrderArbitrageStrategy
    if (!ensureValidBigNumber(item.minProfitPerOrder)) {
        throw new Error(`Invalid min profit per order ${item.minProfitPerOrder}`);
    }
    if (typeof item.minProfitRate !== 'number') {
        throw new TypeError(`Invalid min profit rate ${item.minProfitRate}`);
    }
    if (!ensureValidBigNumber(item.maxAmountPerOrder)) {
        throw new Error(`Invalid max amount per order ${item.maxAmountPerOrder}`);
    }
    if (!ensureValidBigNumber(item.minAmountPerOrder)) {
        throw new Error(`Invalid min amount per order ${item.minAmountPerOrder}`);
    }
    if (typeof item.maxShareOfMyBalancePerOrder !== 'number') {
        throw new TypeError(`Invalid max share of my balance per order ${item.maxShareOfMyBalancePerOrder}`);
    }
    // Check above maxShareOfMyBalancePerOrder is less than 100 and greater than 0
    if (item.maxShareOfMyBalancePerOrder > 100 || item.maxShareOfMyBalancePerOrder < 0) {
        throw new Error(`Invalid max share of my balance per order ${item.maxShareOfMyBalancePerOrder}`);
    }
    return item;
}
function validateBalances(balances, enabledNetworksIds) {
    let parsedBalances;
    try {
        parsedBalances = JSON.parse(balances);
    }
    catch {
        throw new Error('Balances must be a valid JSON object');
    }
    if (parsedBalances === null || typeof parsedBalances !== 'object' || Array.isArray(parsedBalances)) {
        throw new TypeError('Balances must be a valid JSON object');
    }
    for (const [networkId, value] of Object.entries(parsedBalances)) {
        if (!enabledNetworksIds.includes(networkId)) {
            throw new Error(`Network ${networkId} is not supported`);
        }
        if (!ethers_1.BigNumber.isBigNumber(ethers_1.BigNumber.from(value))) {
            throw new Error(`Invalid balance value for network ${networkId}`);
        }
    }
    return true;
}
function isValidAddress(value) {
    return ethers_1.ethers.utils.isAddress(value);
}
function isSupportedAssetNumberT3rn(asset) {
    // Make sure the asset is a number
    if (typeof asset !== 'number') {
        return false;
    }
    const validT3rnAssetNumbers = Object.keys(enums_1.assetNumberT3rnToAssetNamePriceProvider);
    return validT3rnAssetNumbers.includes(asset.toString());
}
function isOrderIdValid(orderId) {
    try {
        ethers_1.ethers.utils.arrayify(orderId);
        return true;
    }
    catch {
        return false;
    }
}
function canBeValidBigNumber(value) {
    return ethers_1.BigNumber.from(value);
}
async function checkAssetBalance(walletAddress, asset, networkId, assetContract) {
    const assetName = enums_1.assetNumberT3rnToAssetNamePriceProvider[asset];
    if (!assetName) {
        logger_1.logger.error({ networkId, asset }, `🍑🚨 Asset not found in config. Return zero as balance`);
        return ethers_1.BigNumber.from(0);
    }
    try {
        const assetAddress = getAssetAddressByNumber(networkId, asset);
        if (assetAddress === ethers_1.ethers.constants.AddressZero) {
            logger_1.logger.warn({
                networkId,
                asset,
                assetName,
            }, `🍑 Asset not found on network. Return zero as balance`);
            return ethers_1.BigNumber.from(0);
        }
        return (await assetContract.balanceOf(walletAddress));
    }
    catch {
        logger_1.logger.error({
            networkId,
            asset,
        }, `🍑🚨 Asset address not found in config. Return zero as balance`);
        return ethers_1.BigNumber.from(0);
    }
}
/**
 * Fetches the list of assets (and their addresses) for the given network, from the map of
 * assets per network.
 *
 * @param {NetworkIdT3rn} networkId
 * @returns {AssetAndAddress[]}
 */
function getAssetsForNetwork(networkId) {
    const currentEnv = process.env.ENVIRONMENT || 'devnet';
    try {
        const contractsConfig = types_1.idToContractsConfig[networkId];
        const perEnvTokens = contractsConfig[currentEnv].tokens;
        let assetAndAddress = Object.keys(perEnvTokens).map((key) => {
            return {
                asset: key.toLowerCase(),
                address: perEnvTokens[key] && perEnvTokens[key] !== 'N/A'
                    ? perEnvTokens[key]
                    : ethers_1.ethers.constants.AddressZero,
            };
        });
        // Populate Native currency on zero address
        assetAndAddress = [
            {
                asset: types_1.idToNativeAssetName[networkId],
                address: ethers_1.ethers.constants.AddressZero,
            },
        ].concat(assetAndAddress);
        logger_1.logger.trace({ networkId, assetAndAddress, perEnvTokens, currentEnv }, '🍑 Loaded contracts config');
        return assetAndAddress;
    }
    catch (error) {
        logger_1.logger.warn({ networkId, error: error.message }, '🚨 Could not load contracts config for tokens automatically. Using hardcoded values');
    }
    const assetsForNetwork = assetAddresses_1.networkToAssetAddressOnPriceProvider.get(networkId);
    if (!assetsForNetwork) {
        throw new Error(`Network not defined in asset-address map. Network: ${networkId}`);
    }
    if (assetsForNetwork.length === 0) {
        throw new Error(`Network has no defined assets in asset-address map. Network: ${networkId}`);
    }
    return assetsForNetwork;
}
/**
 * Fetches the list of assets (and their addresses) for the given network, from Moralis,
 * filtered by enabled assets defined in our contracts config.
 *
 * @param {NetworkIdT3rn} networkId
 * @returns {AssetAndAddressWithT3[]}
 */
function getAssetsForNetworkMoralis(networkId) {
    const currentEnv = process.env.ENVIRONMENT || 'devnet';
    // Load enabled tokens and addresses from our contracts config
    let enabledAssetAndAddress = [];
    try {
        const contractsConfig = types_1.idToContractsConfig[networkId];
        const perEnvTokens = contractsConfig[currentEnv].tokens;
        enabledAssetAndAddress = Object.keys(perEnvTokens).map((key) => ({
            asset: key.toLowerCase(),
            address: perEnvTokens[key] && perEnvTokens[key] !== 'N/A' ? perEnvTokens[key] : ethers_1.ethers.constants.AddressZero,
        }));
        if (!(0, helpers_1.isNonNativeEth)(networkId)) {
            // Populate ETH on zero address
            enabledAssetAndAddress = [
                {
                    asset: enums_1.AssetName.ETH,
                    address: ethers_1.ethers.constants.AddressZero,
                },
            ].concat(enabledAssetAndAddress);
        }
    }
    catch (error) {
        logger_1.logger.warn({ networkId, error: error.message }, '🚨 Could not load enabled assets from contracts config for Moralis.');
    }
    if (!enabledAssetAndAddress || enabledAssetAndAddress.length === 0) {
        throw new Error(`Contract tokens addresses not defined for network ${networkId}`);
    }
    const allMoralisAssetsForNetwork = assetAddressesMoralis_1.networkToAssetAddressOnMoralis.get(networkId);
    if (!allMoralisAssetsForNetwork || allMoralisAssetsForNetwork.length === 0) {
        throw new Error(`Moralis has no defined assets for network: ${networkId}`);
    }
    // Filter Moralis addresses based on our enabled tokens
    const enabledAssetsMap = new Map(enabledAssetAndAddress.map((item) => [item.asset, item.address]));
    const filteredAssets = allMoralisAssetsForNetwork
        .filter(({ asset }) => enabledAssetsMap.has(asset))
        .map((item) => ({
        asset: item.asset,
        address: item.address,
        t3Address: enabledAssetsMap.get(item.asset),
    }));
    if (filteredAssets.length === 0) {
        throw new Error(`No matching Moralis assets found after filtering for network: ${networkId}`);
    }
    logger_1.logger.trace({ networkId, filteredAssets, currentEnv }, '🍑 Loaded Moralis filtered assets');
    return filteredAssets;
}
/**
 * Fetches the list of assets (and their addresses) for the given network, from the map of
 * Pyth assets per network.
 *
 * @param {NetworkIdT3rn} networkId
 * @returns {AssetAndAddress[]}
 */
function getAssetsForNetworkPyth(networkId) {
    const currentEnv = process.env.ENVIRONMENT || 'devnet';
    // Load assets and addresses from our contract config
    let enabledAssetAndAddress = [];
    try {
        const contractsConfig = types_1.idToContractsConfig[networkId];
        const perEnvTokens = contractsConfig[currentEnv].tokens;
        enabledAssetAndAddress = Object.keys(perEnvTokens).map((key) => ({
            asset: key.toLowerCase(),
            address: perEnvTokens[key] && perEnvTokens[key] !== 'N/A' ? perEnvTokens[key] : ethers_1.ethers.constants.AddressZero,
        }));
        // Populate Native on zero address
        enabledAssetAndAddress = [
            {
                asset: types_1.idToNativeAssetName[networkId],
                address: ethers_1.ethers.constants.AddressZero,
            },
        ].concat(enabledAssetAndAddress);
    }
    catch (error) {
        logger_1.logger.warn({ networkId, error: error.message }, '🚨 Could not load enabled assets from contracts config.');
    }
    if (!enabledAssetAndAddress || enabledAssetAndAddress.length === 0) {
        throw new Error(`Contract tokens addresses not defined for network ${networkId}`);
    }
    const allPythAssetsForNetwork = assetAddressesPyth_1.networkToAssetAddressOnPyth.get(networkId);
    if (!allPythAssetsForNetwork || allPythAssetsForNetwork.length === 0) {
        throw new Error(`PYTH network has no defined assets. Network: ${networkId}`);
    }
    // Filter Pyth addresses based on our enabled tokens
    const enabledAssetsMap = new Map(enabledAssetAndAddress.map((item) => [item.asset, item.address]));
    const filteredAssets = allPythAssetsForNetwork
        .filter(({ asset }) => enabledAssetsMap.has(asset))
        .map((item) => ({
        asset: item.asset,
        address: item.address,
        t3Address: enabledAssetsMap.get(item.asset),
    }));
    if (filteredAssets.length === 0) {
        throw new Error(`No matching PYTH assets found after filtering for network: ${networkId}`);
    }
    return filteredAssets;
}
/**
 * Fetches the asset address for the given network and number.
 *
 * @param {NetworkIdT3rn} networkId
 * @param {number}  assetNumber
 * @param dbgContext
 * @returns {string}
 */
function getAssetAddressByNumber(networkId, assetNumber, dbgContext) {
    try {
        const assetName = enums_1.assetNumberT3rnToAssetNamePriceProvider[assetNumber];
        if (!assetName) {
            throw new Error(`Asset number not mapped to a known asset name. AssetNumber: ${assetNumber}`);
        }
        return getAssetAddressByName(networkId, assetName);
    }
    catch (error) {
        logger_1.logger.error({ networkId, assetNumber: assetNumber, error: error.message, dbgContext }, '🚨 Could not get asset address by number');
        throw new Error(`Could not get asset address by number. Network: ${networkId} AssetNumber: ${assetNumber}`);
    }
}
/**
 * Fetches the asset number for the given network and address.
 *
 * @param {NetworkIdT3rn} networkId
 * @param {number}  assetAddress
 * @returns {string}
 */
function getAssetNumberByAddress(networkId, assetAddress) {
    const assetName = getAssetNameByAddress(networkId, assetAddress);
    const assetNumber = enums_1.assetNamePriceProviderToAssetNumberT3rn[assetName];
    // checking if (!asset) will return false if found asset is 0 (eth), validate instead
    if (assetNumber === undefined || assetNumber === null) {
        throw new Error(`Asset name not mapped to a known asset number. AssetName: ${assetName}`);
    }
    return assetNumber;
}
/**
 * Fetches the asset address for the given network and name.
 *
 * @param {NetworkIdT3rn} networkId
 * @param {AssetName}  assetName
 * @returns {string}
 */
function getAssetAddressByName(networkId, assetName) {
    const assetsForNetwork = getAssetsForNetwork(networkId);
    const assetAndAddressObj = assetsForNetwork.find((obj) => obj.asset === assetName);
    if (!assetAndAddressObj) {
        // @ts-ignore - assetNameToT3rnAlias is not defined in the types
        const assetAlias = enums_1.assetNameToT3rnAlias[assetName];
        const addressByAlias = assetsForNetwork.find((obj) => obj.asset === assetAlias);
        if (addressByAlias) {
            logger_1.logger.trace({ assetName, assetAlias, addressByAlias }, '🍑 Found asset by alias');
            return addressByAlias.address;
        }
        throw new Error(`Asset name not defined for given network in asset-address map. Network: ${networkId} Asset: ${assetName}`);
    }
    return assetAndAddressObj.address;
}
/**
 * Fetches the asset name for the given network and address.
 *
 * @param {NetworkIdT3rn} networkId
 * @param {string}  assetAddress
 * @returns {AssetName}
 */
function getAssetNameByAddress(networkId, assetAddress) {
    const assetsForNetwork = getAssetsForNetwork(networkId);
    if (assetAddress === ethers_1.ethers.constants.AddressZero) {
        return types_1.idToNativeAssetName[networkId] || enums_1.AssetName.ETH;
    }
    const assetAndAddressObj = assetsForNetwork.find((obj) => {
        if (!obj.address) {
            return false;
        }
        return obj.address.toLowerCase() === assetAddress.toLowerCase();
    });
    if (!assetAndAddressObj) {
        throw new Error(`Asset address not defined for given network in asset-address map. Network: ${networkId} Address: ${assetAddress}`);
    }
    return assetAndAddressObj.asset;
}
// Retrieve and serialize orders from Redis Cache
async function getCachedOrders(redisClient, cacheKey) {
    const cachedOrders = await redisClient.get(cacheKey);
    if (!cachedOrders) {
        return [];
    }
    logger_1.logger.debug({ cacheKey, time: Date.now() }, 'Retrieved from Redis Cache');
    const fromCache = JSON.parse(cachedOrders);
    const fromCacheParsed = [];
    for (const order of fromCache) {
        try {
            const parsed = {
                ...order,
                amount: ethers_1.BigNumber.from(order.amount),
                maxReward: ethers_1.BigNumber.from(order.maxReward),
                orderTimestamp: ethers_1.BigNumber.from(order.orderTimestamp).toNumber(),
                insurance: ethers_1.BigNumber.from(order.insurance),
            };
            fromCacheParsed.push(parsed);
        }
        catch (error) {
            logger_1.logger.debug({
                id: order.id,
                amount: order.amount,
                maxReward: order.maxReward,
                error: error.message,
            }, 'Error parsing order from cache');
        }
    }
    return fromCacheParsed;
}
// Cache the orders in Redis
async function setCachedOrders(redisClient, orders, cacheKey, expireSec) {
    await redisClient.set(cacheKey, JSON.stringify(orders), { EX: expireSec });
    logger_1.logger.debug({ cacheKey, expireSec, time: Date.now() }, 'Set in Redis Cache');
}
/**
 * Tries to fetch the price for an asset from the public Coinbase API.
 */
async function crawlPrice(asset, prometheus) {
    let assetName = asset;
    if (asset.includes('t3')) {
        assetName = enums_1.t3rnAliasToAssetName[asset];
    }
    assetName = assetName.toUpperCase();
    const { data } = await axios_1.default.get(`https://api.coinbase.com/v2/prices/${assetName}-USD/buy`);
    if (data.error) {
        throw new Error(`Crawler could not fetch price for ${asset}`);
    }
    const price = data.data.amount;
    if (asset === enums_1.AssetName.ETH) {
        // update prometheus metrics
        prometheus.currentEthPrice.set({
            provider: types_1.AssetPriceProviders.Coinbase,
        }, Number.parseFloat(price));
    }
    return price;
}
function omit(obj, keys) {
    try {
        if (!obj)
            return {};
        const clone = { ...obj };
        for (const k of keys) {
            delete clone[k];
        }
        return clone;
    }
    catch {
        return obj;
    }
}
//# sourceMappingURL=helpers.js.map