import { __awaiter, __generator } from "tslib";
import axios from "axios";
import assert from "assert";
import get from "lodash.get";
import { getCoingeckoTokenIdByAddress, retry } from "../utils";
export function msToS(ms) {
    return Math.floor(ms / 1000);
}
// Singleton Coingecko class.
var Coingecko = /** @class */ (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; // seconds
        // Retry configuration.
        this.retryDelay = 1;
        this.numRetries = 0; // Most failures are due to 429 rate-limiting, so there is no point in retrying.
        this.basicApiTimeout = 500; // ms
        this.platformIdMap = new Map(); // chainId => platform_id (137 => "polygon-pos")
        this.tokenIdMap = {}; // coinGeckoId => { platform_id : "tokenAddress":}
        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) {
            assert(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 __awaiter(this, void 0, void 0, function () {
            var id, platforms;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        id = this.platformIdMap.get(chainId);
                        if (id) {
                            return [2 /*return*/, id];
                        }
                        return [4 /*yield*/, 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]; }));
                        // Extend the platformIdMap with any custom platform ids
                        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 /*return*/, id];
                }
            });
        });
    };
    // for tokens not found in our constants, we can attempt to fetch the id from coingecko itself
    Coingecko.prototype.getCoingeckoTokenId = function (address, chainId) {
        return __awaiter(this, void 0, void 0, function () {
            var id, platformId, message;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        try {
                            id = getCoingeckoTokenIdByAddress(address, chainId);
                            return [2 /*return*/, 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 /*yield*/, this.getPlatformId(chainId)];
                    case 1:
                        platformId = _a.sent();
                        id = this.getTokenIdFromAddress(address, platformId);
                        if (id) {
                            return [2 /*return*/, id];
                        }
                        return [4 /*yield*/, 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 /*return*/, 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 __awaiter(this, void 0, void 0, function () {
            var rawTokenList;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, 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 /*return*/, this.tokenIdMap];
                }
            });
        });
    };
    // Fetch historic prices for a `contract` denominated in `currency` between timestamp `from` and `to`. Note timestamps
    // are assumed to be js timestamps and are converted to unixtimestamps by dividing by 1000.
    Coingecko.prototype.getHistoricContractPrices = function (contract, from, to, currency) {
        if (currency === void 0) { currency = "usd"; }
        return __awaiter(this, void 0, void 0, function () {
            var _from, _to, result;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        assert(contract, "requires contract address");
                        assert(currency, "requires currency symbol");
                        assert(from, "requires from timestamp");
                        assert(to, "requires to timestamp");
                        _from = msToS(from);
                        _to = msToS(to);
                        return [4 /*yield*/, 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();
                        // fyi timestamps are returned in ms in contrast to the current price endpoint
                        if (result.prices)
                            return [2 /*return*/, result.prices];
                        throw new Error("Something went wrong fetching coingecko prices!");
                }
            });
        });
    };
    /**
     * Get the current price of a token denominated in `currency`.
     * @param contractAddress The L1 token address to fetch the price for.
     * @param date A datestring in the format "dd-mm-yyyy" to fetch the price for.
     * @param currency The currency to fetch the price in. Defaults to "usd".
     * @returns The price of the token at the given date.
     * @throws If today is selected and it is before 3am UTC or if the price is not found.
     */
    Coingecko.prototype.getContractHistoricDayPrice = function (contractAddress, date, currency, chainId) {
        var _a, _b;
        if (currency === void 0) { currency = "usd"; }
        if (chainId === void 0) { chainId = 1; }
        return __awaiter(this, void 0, void 0, function () {
            var coingeckoTokenIdentifier, url, queryParams, result, price;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0: return [4 /*yield*/, this.getCoingeckoTokenId(contractAddress, chainId)];
                    case 1:
                        coingeckoTokenIdentifier = _c.sent();
                        assert(date, "Requires date string");
                        url = "coins/".concat(coingeckoTokenIdentifier, "/history");
                        queryParams = {
                            date: date,
                            localization: "false",
                        };
                        return [4 /*yield*/, 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];
                        assert(price, "No price found for ".concat(contractAddress, " on ").concat(date));
                        return [2 /*return*/, 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 __awaiter(this, void 0, void 0, function () {
            var platform_id, priceCache, tokenPrice;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, 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 /*break*/, 3];
                        return [4 /*yield*/, this.getContractPrices([contractAddress], currency, platform_id)];
                    case 2:
                        _a.sent();
                        tokenPrice = priceCache[contractAddress];
                        _a.label = 3;
                    case 3:
                        assert(tokenPrice !== undefined);
                        return [2 /*return*/, [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 __awaiter(this, void 0, void 0, function () {
            var platform_id, priceCache, tokenPrice, coingeckoId, result, cgPrice, errMsg;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, 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 /*break*/, 4];
                        return [4 /*yield*/, this.getCoingeckoTokenId(contractAddress, chainId)];
                    case 2:
                        coingeckoId = _a.sent();
                        return [4 /*yield*/, 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];
                        assert(tokenPrice !== undefined);
                        return [2 /*return*/, [tokenPrice.timestamp.toString(), tokenPrice.price]];
                }
            });
        });
    };
    // Return an array of spot prices for an array of collateral addresses in one async call. Note we might in future
    // This was adapted from packages/merkle-distributor/kpi-options-helpers/calculate-uma-tvl.ts
    Coingecko.prototype.getContractPrices = function (addresses, currency, platform_id) {
        if (currency === void 0) { currency = "usd"; }
        if (platform_id === void 0) { platform_id = "ethereum"; }
        return __awaiter(this, void 0, void 0, function () {
            var priceCache, contract_addresses, result, err_1, errMsg;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        priceCache = this.getPriceCache(currency, platform_id);
                        // Pre-populate price cache with requested token addresses
                        addresses.forEach(function (addr) {
                            if (priceCache[addr] === undefined) {
                                priceCache[addr] = { address: addr, price: 0, timestamp: 0 };
                            }
                        });
                        contract_addresses = Object.keys(priceCache);
                        assert(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 /*yield*/, 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:
                        // Coingecko expects a comma-delimited (%2c) list.
                        result = _a.sent();
                        return [3 /*break*/, 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:
                        // Note: contract_addresses is a reliable reference for the price lookup.
                        // priceCache might have been updated subsequently by concurrent price requests.
                        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 /*return*/, 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 __awaiter(_this, void 0, void 0, function () {
            var proHost, err_2;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        proHost = this.proHost;
                        if (!(this.apiKey === undefined)) return [3 /*break*/, 2];
                        return [4 /*yield*/, this._callBasic(path)];
                    case 1: return [2 /*return*/, (_a.sent())];
                    case 2:
                        _a.trys.push([2, 4, , 6]);
                        return [4 /*yield*/, this._callBasic(path, this.basicApiTimeout)];
                    case 3: return [2 /*return*/, (_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 /*yield*/, this._callPro(path)];
                    case 5: return [2 /*return*/, _a.sent()];
                    case 6: return [2 /*return*/];
                }
            });
        }); };
        // Note: If a pro API key is configured, there is no need to retry as the Pro API will act as the basic's fall back.
        return 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 __awaiter(this, void 0, void 0, function () {
            var url, result, err_3, msg;
            return __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 /*yield*/, axios(url, { timeout: timeout })];
                    case 2:
                        result = _a.sent();
                        return [2 /*return*/, result.data];
                    case 3:
                        err_3 = _a.sent();
                        msg = get(err_3, "response.data.error", get(err_3, "response.statusText", err_3.message));
                        throw new Error(msg);
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    Coingecko.prototype._callPro = function (path) {
        return __awaiter(this, void 0, void 0, function () {
            var url, result, err_4, msg;
            return __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 /*yield*/, axios(url, { params: { x_cg_pro_api_key: this.apiKey } })];
                    case 2:
                        result = _a.sent();
                        return [2 /*return*/, result.data];
                    case 3:
                        err_4 = _a.sent();
                        msg = get(err_4, "response.data.error", get(err_4, "response.statusText", err_4.message));
                        throw new Error(msg);
                    case 4: return [2 /*return*/];
                }
            });
        });
    };
    return Coingecko;
}());
export { Coingecko };
//# sourceMappingURL=Coingecko.js.map