|
| 1 | +/* |
| 2 | + Copyright 2021 Set Labs Inc. |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +'use strict'; |
| 18 | + |
| 19 | +const pageResults = require('graph-results-pager'); |
| 20 | + |
| 21 | +import axios from 'axios'; |
| 22 | +import Assertions from '../../../assertions'; |
| 23 | + |
| 24 | +import { |
| 25 | + CoinGeckoCoinPrices, |
| 26 | + CoinGeckoTokenData, |
| 27 | + SushiswapTokenData, |
| 28 | + CoinGeckoTokenMap, |
| 29 | + CoinPricesParams, |
| 30 | + PolygonMappedTokenData |
| 31 | +} from '../../../types'; |
| 32 | + |
| 33 | +/** |
| 34 | + * These currency codes can be used for the vs_currencies parameter of the service's |
| 35 | + * fetchCoinPrices method |
| 36 | + * |
| 37 | + * @type {number} |
| 38 | + */ |
| 39 | +export const USD_CURRENCY_CODE = 'usd'; |
| 40 | +export const ETH_CURRENCY_CODE = 'eth'; |
| 41 | + |
| 42 | +/** |
| 43 | + * @title CoinGeckoDataService |
| 44 | + * @author Set Protocol |
| 45 | + * |
| 46 | + * A utility library for fetching token metadata and coin prices from Coingecko for Ethereum |
| 47 | + * and Polygon chains |
| 48 | + */ |
| 49 | +export class CoinGeckoDataService { |
| 50 | + chainId: number; |
| 51 | + private tokenList: CoinGeckoTokenData[] | undefined; |
| 52 | + private tokenMap: CoinGeckoTokenMap | undefined; |
| 53 | + private assert: Assertions; |
| 54 | + |
| 55 | + constructor(chainId: number) { |
| 56 | + this.assert = new Assertions(); |
| 57 | + this.assert.common.isSupportedChainId(chainId); |
| 58 | + this.chainId = chainId; |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Gets address-to-price map of token prices for a set of token addresses and currencies |
| 63 | + * |
| 64 | + * @param params CoinPricesParams: token addresses and currency codes |
| 65 | + * @return CoinGeckoCoinPrices: Address to price map |
| 66 | + */ |
| 67 | + async fetchCoinPrices(params: CoinPricesParams): Promise<CoinGeckoCoinPrices> { |
| 68 | + const platform = this.getPlatform(); |
| 69 | + const endpoint = `https://api.coingecko.com/api/v3/simple/token_price/${platform}?`; |
| 70 | + const contractAddressParams = `contract_addresses=${params.contractAddresses.join(',')}`; |
| 71 | + const vsCurrenciesParams = `vs_currencies=${params.vsCurrencies.join(',')}`; |
| 72 | + const url = `${endpoint}${contractAddressParams}&${vsCurrenciesParams}`; |
| 73 | + |
| 74 | + const response = await axios.get(url); |
| 75 | + return response.data; |
| 76 | + } |
| 77 | + |
| 78 | + /** |
| 79 | + * Gets a list of available tokens and their metadata for chain. If Ethereum, the list |
| 80 | + * is sourced from Uniswap. If Polygon the list is sourced from Sushiswap with image assets |
| 81 | + * derived from multiple sources including CoinGecko |
| 82 | + * |
| 83 | + * @return CoinGeckoTokenData: array of token data |
| 84 | + */ |
| 85 | + async fetchTokenList(): Promise<CoinGeckoTokenData[]> { |
| 86 | + if (this.tokenList !== undefined) return this.tokenList; |
| 87 | + |
| 88 | + switch (this.chainId) { |
| 89 | + case 1: |
| 90 | + this.tokenList = await this.fetchEthereumTokenList(); |
| 91 | + break; |
| 92 | + case 137: |
| 93 | + this.tokenList = await this.fetchPolygonTokenList(); |
| 94 | + break; |
| 95 | + } |
| 96 | + this.tokenMap = this.convertTokenListToAddressMap(this.tokenList); |
| 97 | + |
| 98 | + return this.tokenList!; |
| 99 | + } |
| 100 | + |
| 101 | + /** |
| 102 | + * Gets a token list (see above) formatted as an address indexed map |
| 103 | + * |
| 104 | + * @return CoinGeckoTokenMap: map of token addresses to token metadata |
| 105 | + */ |
| 106 | + async fetchTokenMap(): Promise<CoinGeckoTokenMap> { |
| 107 | + if (this.tokenMap !== undefined) return this.tokenMap; |
| 108 | + |
| 109 | + this.tokenList = await this.fetchTokenList(); |
| 110 | + this.tokenMap = this.convertTokenListToAddressMap(this.tokenList); |
| 111 | + |
| 112 | + return this.tokenMap; |
| 113 | + } |
| 114 | + |
| 115 | + private async fetchEthereumTokenList(): Promise<CoinGeckoTokenData[]> { |
| 116 | + const url = 'https://tokens.coingecko.com/uniswap/all.json'; |
| 117 | + const response = await axios.get(url); |
| 118 | + return response.data.tokens; |
| 119 | + } |
| 120 | + |
| 121 | + private async fetchPolygonTokenList(): Promise<CoinGeckoTokenData[]> { |
| 122 | + const coingeckoEthereumTokens = await this.fetchEthereumTokenList(); |
| 123 | + const polygonMappedTokens = await this.fetchPolygonMappedTokenList(); |
| 124 | + const sushiPolygonTokenList = await this.fetchSushiPolygonTokenList(); |
| 125 | + const quickswapPolygonTokenList = await this.fetchQuickswapPolygonTokenList(); |
| 126 | + |
| 127 | + for (const token of sushiPolygonTokenList) { |
| 128 | + const quickswapToken = quickswapPolygonTokenList.find(t => t.address.toLowerCase() === token.address); |
| 129 | + |
| 130 | + if (quickswapToken) { |
| 131 | + token.logoURI = quickswapToken.logoURI; |
| 132 | + continue; |
| 133 | + } |
| 134 | + |
| 135 | + const ethereumAddress = polygonMappedTokens[token.address]; |
| 136 | + |
| 137 | + if (ethereumAddress !== undefined) { |
| 138 | + const ethereumToken = coingeckoEthereumTokens.find(t => t.address.toLowerCase() === ethereumAddress); |
| 139 | + |
| 140 | + if (ethereumToken) { |
| 141 | + token.logoURI = ethereumToken.logoURI; |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + |
| 146 | + return sushiPolygonTokenList; |
| 147 | + } |
| 148 | + |
| 149 | + private async fetchSushiPolygonTokenList() { |
| 150 | + let tokens: SushiswapTokenData[] = []; |
| 151 | + const url = 'https://api.thegraph.com/subgraphs/name/sushiswap/matic-exchange'; |
| 152 | + const properties = [ |
| 153 | + 'id', |
| 154 | + 'symbol', |
| 155 | + 'name', |
| 156 | + 'decimals', |
| 157 | + 'volumeUSD', |
| 158 | + ]; |
| 159 | + |
| 160 | + const response = await pageResults({ |
| 161 | + api: url, |
| 162 | + query: { |
| 163 | + entity: 'tokens', |
| 164 | + properties: properties, |
| 165 | + }, |
| 166 | + }); |
| 167 | + |
| 168 | + for (const token of response) { |
| 169 | + tokens.push({ |
| 170 | + chainId: 137, |
| 171 | + address: token.id, |
| 172 | + symbol: token.symbol, |
| 173 | + name: token.name, |
| 174 | + decimals: parseInt(token.decimals), |
| 175 | + volumeUSD: parseFloat(token.volumeUSD), |
| 176 | + }); |
| 177 | + } |
| 178 | + |
| 179 | + // Sort by volume and filter out untraded tokens |
| 180 | + tokens.sort((a, b) => b.volumeUSD - a.volumeUSD); |
| 181 | + tokens = tokens.filter(t => t.volumeUSD > 0); |
| 182 | + |
| 183 | + return tokens; |
| 184 | + } |
| 185 | + |
| 186 | + private async fetchPolygonMappedTokenList(): Promise<PolygonMappedTokenData> { |
| 187 | + let offset = 0; |
| 188 | + const tokens: PolygonMappedTokenData = {}; |
| 189 | + |
| 190 | + const url = 'https://tokenmapper.api.matic.today/api/v1/mapping?'; |
| 191 | + const params = 'map_type=[%22POS%22]&chain_id=137&limit=200&offset='; |
| 192 | + |
| 193 | + while (true) { |
| 194 | + const response = await axios.get(`${url}${params}${offset}`); |
| 195 | + |
| 196 | + if (response.data.message === 'success') { |
| 197 | + for (const token of response.data.data.mapping) { |
| 198 | + tokens[token.child_token.toLowerCase()] = token.root_token.toLowerCase(); |
| 199 | + } |
| 200 | + |
| 201 | + if (response.data.data.has_next_page === true) { |
| 202 | + offset += 200; |
| 203 | + continue; |
| 204 | + } |
| 205 | + } |
| 206 | + break; |
| 207 | + } |
| 208 | + |
| 209 | + return tokens; |
| 210 | + } |
| 211 | + |
| 212 | + private async fetchQuickswapPolygonTokenList(): Promise<CoinGeckoTokenData[]> { |
| 213 | + const url = 'https://raw.githubusercontent.com/sameepsi/' + |
| 214 | + 'quickswap-default-token-list/master/src/tokens/mainnet.json'; |
| 215 | + |
| 216 | + const data = (await axios.get(url)).data; |
| 217 | + return data; |
| 218 | + } |
| 219 | + |
| 220 | + private convertTokenListToAddressMap(list: CoinGeckoTokenData[] = []): CoinGeckoTokenMap { |
| 221 | + const tokenMap: CoinGeckoTokenMap = {}; |
| 222 | + |
| 223 | + for (const entry of list) { |
| 224 | + tokenMap[entry.address] = Object.assign({}, entry); |
| 225 | + } |
| 226 | + |
| 227 | + return tokenMap; |
| 228 | + } |
| 229 | + |
| 230 | + private getPlatform(): string { |
| 231 | + switch (this.chainId) { |
| 232 | + case 1: return 'ethereum'; |
| 233 | + case 137: return 'polygon-pos'; |
| 234 | + default: return ''; |
| 235 | + } |
| 236 | + } |
| 237 | +} |
0 commit comments