"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Coingecko = exports.msToS = void 0;
var tslib_1 = require("tslib");
var axios_1 = tslib_1.__importDefault(require("axios"));
var assert_1 = tslib_1.__importDefault(require("assert"));
var lodash_get_1 = tslib_1.__importDefault(require("lodash.get"));
var utils_1 = require("../utils");
function msToS(ms) {
    return Math.floor(ms / 1000);
}
exports.msToS = msToS;
var Coingecko = (function () {
    function Coingecko(host, proHost, logger, apiKey, customPlatformIdMap) {
        this.host = host;
        this.proHost = proHost;
        this.logger = logger;
        this.apiKey = apiKey;
        this.customPlatformIdMap = customPlatformIdMap;
        this._maxPriceAge = 300;
        this.retryDelay = 1;
        this.numRetries = 0;
        this.basicApiTimeout = 500;
        this.platformIdMap = new Map();
        this.tokenIdMap = {};
        this.prices = {};
    }
    Coingecko.get = function (logger, apiKey, customPlatformIdMap) {
        if (!this.instance)
            this.instance = new Coingecko("https://api.coingecko.com/api/v3", "https://pro-api.coingecko.com/api/v3", logger, apiKey, customPlatformIdMap);
        return this.instance;
    };
    Object.defineProperty(Coingecko.prototype, "maxPriceAge", {
        get: function () {
            return this._maxPriceAge;
        },
        set: function (age) {
            (0, assert_1.default)(age >= 0);
            this.logger.debug({
                at: "Coingecko#maxPriceAge",
                message: "Setting maxPriceAge (S) ".concat(this._maxPriceAge, " => ").concat(age, "."),
            });
            this._maxPriceAge = age;
        },
        enumerable: false,
        configurable: true
    });
    Coingecko.prototype.getPlatformId = function (chainId) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var id, platforms;
            var _this = this;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        id = this.platformIdMap.get(chainId);
                        if (id) {
                            return [2, id];
                        }
                        return [4, this.getPlatforms()];
                    case 1:
                        platforms = _a.sent();
                        this.platformIdMap = new Map(platforms.filter(function (chain) { return Boolean(chain.chain_identifier); }).map(function (chain) { return [chain.chain_identifier, chain.id]; }));
                        if (this.customPlatformIdMap) {
                            Object.entries(this.customPlatformIdMap).forEach(function (_a) {
                                var chainId = _a[0], platformId = _a[1];
                                _this.platformIdMap.set(Number(chainId), platformId);
                            });
                        }
                        id = this.platformIdMap.get(chainId);
                        if (!id) {
                            this.logger.error({
                                message: "Coingecko does not support chain with id ".concat(chainId),
                                at: "Coingecko#getPlatformId",
                            });
                            throw new Error("Coingecko does not support chain with id ".concat(chainId));
                        }
                        return [2, id];
                }
            });
        });
    };
    Coingecko.prototype.getCoingeckoTokenId = function (address, chainId) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var id, platformId, message;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        try {
                            id = (0, utils_1.getCoingeckoTokenIdByAddress)(address, chainId);
                            return [2, id];
                        }
                        catch (error) {
                            this.logger.warn({
                                at: "Coingecko#getCoingeckoTokenIdByAddress",
                                message: "Token with address ".concat(address, " not found in constants. Attempting to fetch ID from coingecko API..."),
                            });
                        }
                        return [4, this.getPlatformId(chainId)];
                    case 1:
                        platformId = _a.sent();
                        id = this.getTokenIdFromAddress(address, platformId);
                        if (id) {
                            return [2, id];
                        }
                        return [4, this.updateTokenMap()];
                    case 2:
                        _a.sent();
                        id = this.getTokenIdFromAddress(address, platformId);
                        if (!id) {
                            message = "Coin with address ".concat(address, " does not exist on chain with id ").concat(chainId);
                            this.logger.error({
                                at: "Coingecko#getCoingeckoTokenIdByAddress",
                                message: message,
                            });
                            throw new Error(message);
                        }
                        return [2, id];
                }
            });
        });
    };
    Coingecko.prototype.getTokenIdFromAddress = function (address, platformId) {
        var _a;
        return (_a = Object.entries(this.tokenIdMap).find(function (_a) {
            var _b;
            var _ = _a[0], value = _a[1];
            return Boolean(((_b = value === null || value === void 0 ? void 0 : value[platformId]) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === address.toLowerCase());
        })) === null || _a === void 0 ? void 0 : _a[0];
    };
    Coingecko.prototype.updateTokenMap = function () {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var rawTokenList;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4, this.call("coins/list?include_platform=true")];
                    case 1:
                        rawTokenList = _a.sent();
                        this.tokenIdMap = Object.fromEntries(rawTokenList
                            .filter(function (token) { var _a; return Boolean(((_a = Object.values(token === null || token === void 0 ? void 0 : token.platforms)) === null || _a === void 0 ? void 0 : _a.length) > 0); })
                            .map(function (token) { return [token.id, token.platforms]; }));
                        return [2, this.tokenIdMap];
                }
            });
        });
    };
    Coingecko.prototype.getHistoricContractPrices = function (contract, from, to, currency) {
        if (currency === void 0) { currency = "usd"; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var _from, _to, result;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        (0, assert_1.default)(contract, "requires contract address");
                        (0, assert_1.default)(currency, "requires currency symbol");
                        (0, assert_1.default)(from, "requires from timestamp");
                        (0, assert_1.default)(to, "requires to timestamp");
                        _from = msToS(from);
                        _to = msToS(to);
                        return [4, this.call("coins/ethereum/contract/".concat(contract.toLowerCase(), "/market_chart/range/?vs_currency=").concat(currency, "&from=").concat(_from, "&to=").concat(_to))];
                    case 1:
                        result = _a.sent();
                        if (result.prices)
                            return [2, result.prices];
                        throw new Error("Something went wrong fetching coingecko prices!");
                }
            });
        });
    };
    Coingecko.prototype.getContractHistoricDayPrice = function (contractAddress, date, currency, chainId) {
        var _a, _b;
        if (currency === void 0) { currency = "usd"; }
        if (chainId === void 0) { chainId = 1; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var coingeckoTokenIdentifier, url, queryParams, result, price;
            return tslib_1.__generator(this, function (_c) {
                switch (_c.label) {
                    case 0: return [4, this.getCoingeckoTokenId(contractAddress, chainId)];
                    case 1:
                        coingeckoTokenIdentifier = _c.sent();
                        (0, assert_1.default)(date, "Requires date string");
                        url = "coins/".concat(coingeckoTokenIdentifier, "/history");
                        queryParams = {
                            date: date,
                            localization: "false",
                        };
                        return [4, this.call("".concat(url, "?").concat(new URLSearchParams(queryParams).toString()))];
                    case 2:
                        result = _c.sent();
                        price = (_b = (_a = result === null || result === void 0 ? void 0 : result.market_data) === null || _a === void 0 ? void 0 : _a.current_price) === null || _b === void 0 ? void 0 : _b[currency];
                        (0, assert_1.default)(price, "No price found for ".concat(contractAddress, " on ").concat(date));
                        return [2, price];
                }
            });
        });
    };
    Coingecko.prototype.getContractDetails = function (contract_address, platform_id) {
        if (platform_id === void 0) { platform_id = "ethereum"; }
        return this.call("coins/".concat(platform_id, "/contract/").concat(contract_address.toLowerCase()));
    };
    Coingecko.prototype.getCurrentPriceByContract = function (contractAddress, currency, chainId) {
        if (currency === void 0) { currency = "usd"; }
        if (chainId === void 0) { chainId = 1; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var platform_id, priceCache, tokenPrice;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4, this.getPlatformId(chainId)];
                    case 1:
                        platform_id = _a.sent();
                        priceCache = this.getPriceCache(currency, platform_id);
                        tokenPrice = this.getCachedAddressPrice(contractAddress, currency, platform_id);
                        if (!(tokenPrice === undefined)) return [3, 3];
                        return [4, this.getContractPrices([contractAddress], currency, platform_id)];
                    case 2:
                        _a.sent();
                        tokenPrice = priceCache[contractAddress];
                        _a.label = 3;
                    case 3:
                        (0, assert_1.default)(tokenPrice !== undefined);
                        return [2, [tokenPrice.timestamp.toString(), tokenPrice.price]];
                }
            });
        });
    };
    Coingecko.prototype.getCurrentPriceById = function (contractAddress, currency, chainId) {
        if (currency === void 0) { currency = "usd"; }
        if (chainId === void 0) { chainId = 1; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var platform_id, priceCache, tokenPrice, coingeckoId, result, cgPrice, errMsg;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4, this.getPlatformId(chainId)];
                    case 1:
                        platform_id = _a.sent();
                        priceCache = this.getPriceCache(currency, platform_id);
                        tokenPrice = this.getCachedAddressPrice(contractAddress, currency, platform_id);
                        if (!(tokenPrice === undefined)) return [3, 4];
                        return [4, this.getCoingeckoTokenId(contractAddress, chainId)];
                    case 2:
                        coingeckoId = _a.sent();
                        return [4, this.call("simple/price?ids=".concat(coingeckoId, "&vs_currencies=").concat(currency, "&include_last_updated_at=true"))];
                    case 3:
                        result = _a.sent();
                        cgPrice = result === null || result === void 0 ? void 0 : result[coingeckoId];
                        if (cgPrice === undefined || !(cgPrice === null || cgPrice === void 0 ? void 0 : cgPrice[currency])) {
                            errMsg = "No price found for ".concat(coingeckoId);
                            this.logger.debug({
                                at: "Coingecko#getCurrentPriceById",
                                message: errMsg,
                            });
                            throw new Error(errMsg);
                        }
                        else {
                            this.updatePriceCache(cgPrice, contractAddress, currency, platform_id);
                        }
                        _a.label = 4;
                    case 4:
                        tokenPrice = priceCache[contractAddress];
                        (0, assert_1.default)(tokenPrice !== undefined);
                        return [2, [tokenPrice.timestamp.toString(), tokenPrice.price]];
                }
            });
        });
    };
    Coingecko.prototype.getContractPrices = function (addresses, currency, platform_id) {
        if (currency === void 0) { currency = "usd"; }
        if (platform_id === void 0) { platform_id = "ethereum"; }
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var priceCache, contract_addresses, result, err_1, errMsg;
            var _this = this;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        priceCache = this.getPriceCache(currency, platform_id);
                        addresses.forEach(function (addr) {
                            if (priceCache[addr] === undefined) {
                                priceCache[addr] = { address: addr, price: 0, timestamp: 0 };
                            }
                        });
                        contract_addresses = Object.keys(priceCache);
                        (0, assert_1.default)(contract_addresses.length > 0, "Must supply at least 1 contract address");
                        this.logger.debug({
                            at: "Coingecko#getContractPrices",
                            message: "Updating ".concat(platform_id, "/").concat(currency, " token prices."),
                            tokens: contract_addresses,
                        });
                        result = {};
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4, this.call("simple/token_price/".concat(platform_id, "?contract_addresses=").concat(contract_addresses.join("%2C"), "&vs_currencies=").concat(currency, "&include_last_updated_at=true"))];
                    case 2:
                        result = _a.sent();
                        return [3, 4];
                    case 3:
                        err_1 = _a.sent();
                        errMsg = "Failed to retrieve ".concat(platform_id, "/").concat(currency, " prices (").concat(err_1, ")");
                        this.logger.debug({
                            at: "Coingecko#getContractPrices",
                            message: errMsg,
                            tokens: contract_addresses,
                        });
                        throw new Error(errMsg);
                    case 4:
                        contract_addresses.forEach(function (addr) {
                            var cgPrice = result[addr.toLowerCase()];
                            if (cgPrice === undefined) {
                                _this.logger.debug({
                                    at: "Coingecko#getContractPrices",
                                    message: "Token ".concat(addr, " not included in CoinGecko response."),
                                });
                            }
                            else {
                                _this.updatePriceCache(cgPrice, addr, currency, platform_id);
                            }
                        });
                        return [2, addresses.map(function (addr) { return priceCache[addr]; })];
                }
            });
        });
    };
    Coingecko.prototype.getPlatforms = function () {
        return this.call("asset_platforms");
    };
    Coingecko.prototype.call = function (path) {
        var _this = this;
        var sendRequest = function () { return tslib_1.__awaiter(_this, void 0, void 0, function () {
            var proHost, err_2;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        proHost = this.proHost;
                        if (!(this.apiKey === undefined)) return [3, 2];
                        return [4, this._callBasic(path)];
                    case 1: return [2, (_a.sent())];
                    case 2:
                        _a.trys.push([2, 4, , 6]);
                        return [4, this._callBasic(path, this.basicApiTimeout)];
                    case 3: return [2, (_a.sent())];
                    case 4:
                        err_2 = _a.sent();
                        this.logger.debug({
                            at: "sdk/coingecko",
                            message: "Basic CG url request failed, falling back to CG PRO host ".concat(proHost),
                            errMessage: err_2.message,
                        });
                        return [4, this._callPro(path)];
                    case 5: return [2, _a.sent()];
                    case 6: return [2];
                }
            });
        }); };
        return (0, utils_1.retry)(sendRequest, this.apiKey === undefined ? this.numRetries : 0, this.retryDelay);
    };
    Coingecko.prototype.getPriceCache = function (currency, platform_id) {
        if (this.prices[platform_id] === undefined)
            this.prices[platform_id] = {};
        if (this.prices[platform_id][currency] === undefined)
            this.prices[platform_id][currency] = {};
        return this.prices[platform_id][currency];
    };
    Coingecko.prototype.getCachedAddressPrice = function (contractAddress, currency, platform_id) {
        var priceCache = this.getPriceCache(currency, platform_id);
        var now = msToS(Date.now());
        var tokenPrice = priceCache[contractAddress];
        if (tokenPrice === undefined || tokenPrice.timestamp + this.maxPriceAge <= now) {
            if (this.maxPriceAge > 0) {
                this.logger.debug({
                    at: "Coingecko#getCachedAddressPrice",
                    message: "Cache miss on ".concat(platform_id, "/").concat(currency, " for ").concat(contractAddress),
                    maxPriceAge: this.maxPriceAge,
                    tokenPrice: tokenPrice,
                });
            }
            return undefined;
        }
        else {
            this.logger.debug({
                at: "Coingecko#getCachedAddressPrice",
                message: "Cache hit on token ".concat(contractAddress, " (age ").concat(now - tokenPrice.timestamp, " S)."),
                price: tokenPrice,
            });
            return tokenPrice;
        }
    };
    Coingecko.prototype.updatePriceCache = function (cgPrice, contractAddress, currency, platform_id) {
        var priceCache = this.getPriceCache(currency, platform_id);
        if (priceCache[contractAddress] === undefined) {
            priceCache[contractAddress] = { address: contractAddress, price: 0, timestamp: 0 };
        }
        if (cgPrice.last_updated_at > priceCache[contractAddress].timestamp) {
            priceCache[contractAddress] = {
                address: contractAddress,
                price: cgPrice[currency],
                timestamp: cgPrice.last_updated_at,
            };
            this.logger.debug({
                at: "Coingecko#updatePriceCache",
                message: "Updated ".concat(platform_id, "/").concat(currency, "/").concat(contractAddress, " token price cache."),
            });
        }
        else {
            this.logger.debug({
                at: "Coingecko#updatePriceCache",
                message: "No new price available for token ".concat(contractAddress, "."),
                token: cgPrice,
            });
        }
    };
    Coingecko.prototype._callBasic = function (path, timeout) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var url, result, err_3, msg;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        url = "".concat(this.host, "/").concat(path);
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4, (0, axios_1.default)(url, { timeout: timeout })];
                    case 2:
                        result = _a.sent();
                        return [2, result.data];
                    case 3:
                        err_3 = _a.sent();
                        msg = (0, lodash_get_1.default)(err_3, "response.data.error", (0, lodash_get_1.default)(err_3, "response.statusText", err_3.message));
                        throw new Error(msg);
                    case 4: return [2];
                }
            });
        });
    };
    Coingecko.prototype._callPro = function (path) {
        return tslib_1.__awaiter(this, void 0, void 0, function () {
            var url, result, err_4, msg;
            return tslib_1.__generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        url = "".concat(this.proHost, "/").concat(path);
                        _a.label = 1;
                    case 1:
                        _a.trys.push([1, 3, , 4]);
                        return [4, (0, axios_1.default)(url, { params: { x_cg_pro_api_key: this.apiKey } })];
                    case 2:
                        result = _a.sent();
                        return [2, result.data];
                    case 3:
                        err_4 = _a.sent();
                        msg = (0, lodash_get_1.default)(err_4, "response.data.error", (0, lodash_get_1.default)(err_4, "response.statusText", err_4.message));
                        throw new Error(msg);
                    case 4: return [2];
                }
            });
        });
    };
    return Coingecko;
}());
exports.Coingecko = Coingecko;
//# sourceMappingURL=Coingecko.js.map