import alot from 'alot'; import { JsonArrayStore } from '@dequanto/json/JsonArrayStore'; import { IToken } from '@dequanto/models/IToken'; import { ITokenGlob } from '@dequanto/models/ITokenGlob'; import { TPlatform } from '@dequanto/models/TPlatform'; import { $path } from '@dequanto/utils/$path'; import { File } from 'atma-io'; import { $require } from '@dequanto/utils/$require'; import { l } from '@dequanto/utils/$logger'; import { TAddress } from '@dequanto/models/TAddress'; import { $str } from '@dequanto/solidity/utils/$str'; import { $http } from '@dequanto/utils/$http'; export interface IChainlinkFeedInfo { platform: TPlatform address: TAddress asset: string pair: [ string, string ] } export class ChainlinkFeedProvider { store = new JsonArrayStore ({ path: $path.resolve('/data/chainlink/feeds.json'), key: x => x.address, format: true, }); async getFeeds(): Promise { return await this.store.getAll(); } async getRouteForSymbol (symbol: string, targetSymbol = 'USD', ignoreSymbols: string[] = []): Promise { let allFeeds = await this.getFeeds(); let symbolFeeds = allFeeds.filter(x => x.pair[0] === symbol); let target = symbolFeeds.find(x => x.pair[1] === targetSymbol); if (target) { return [ target ]; } let routes = await alot(symbolFeeds) .mapAsync(async symbolFeed => { let hopSymbol = symbolFeed.pair[1]; if (ignoreSymbols.includes(hopSymbol)) { return null; } let ignore = [ ...ignoreSymbols, symbol ]; let arr = await this.getRouteForSymbol(hopSymbol, targetSymbol, ignore); if (arr == null || arr.length === 0) { return null; } return [ symbolFeed, ...arr ]; }) .filterAsync(x => x != null && x.length > 0) .toArrayAsync(); if (routes.length === 0) { return []; } let smallest = alot(routes).sortBy(x => x.length).first(); return [ ...smallest ] } async redownload () { // from 'https://docs.chain.link/data-feeds/price-feeds/addresses'; const paths = [ { url: 'https://reference-data-directory.vercel.app/feeds-mainnet.json', platform: 'eth' }, { url: 'https://reference-data-directory.vercel.app/feeds-matic-mainnet.json', platform: 'polygon' }, { url: 'https://reference-data-directory.vercel.app/feeds-bsc-mainnet.json', platform: 'bsc' }, { url: 'https://reference-data-directory.vercel.app/feeds-ethereum-mainnet-base-1.json', platform: 'base' }, { url: 'https://reference-data-directory.vercel.app/feeds-ethereum-mainnet-optimism-1.json', platform: 'optimism' } ]; let tokens = await alot(paths).mapManyAsync(async pathInfo => { return this.fetchFeed(pathInfo.url, pathInfo.platform); }).toArrayAsync({ threads: 1 }); l`Fetched ${tokens.length} feeds`; let platformStats = alot(tokens).groupBy(x => x.platform).toDictionary(x => x.key, x => x.values.length); l`Feeds per platform: ${platformStats}`; let uniqueStats = alot(tokens).distinctBy(x => x.pair[0]).toArray().length; l`Feeds with unique base: ${uniqueStats}`; await this.store.saveAll(tokens); return tokens; } private async fetchFeed (path: string, platform: TPlatform) { const resp = await $http.get(path); const tokens = alot(resp.data).map(feed => { let pair: [ string, string ]; if ($str.isNullOrWhiteSpace(feed.pair[0] || feed.pair[1])) { let match = /(?\w+)\s*\/\s*(?\w+)/.exec(feed.name); if (match == null) { console.log(` - Chainlink skipped "${feed.name}" as not a crypto pair`); return null; } pair = [ match.groups.base, match.groups.quote]; } else { pair = feed.pair; } if (feed.contractType ==="verifier") { // skip not onchain oracle contracts return null; } switch (feed.feedCategory) { case 'deprecating': { // skip return null; } default: { if (feed.feedCategory !== 'verified') { console.log(` - The Feed ${feed.name} is not verified ("${feed.feedCategory}")`); } break; } } $require.notNull(feed.proxyAddress, `The proxy address is undefined in ${JSON.stringify(feed)}`); return { platform: platform, address: feed.proxyAddress, asset: feed.assetName ?? feed.name, pair: pair, }; }) .filter(x => x != null) .toArray(); return tokens; } }