@talismn/token-rates 1.0.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,12 @@
1
1
  import { Token, TokenId } from "@talismn/chaindata-provider";
2
- import { TokenRatesList } from "./types";
2
+ import { TokenRateCurrency, TokenRatesList } from "./types";
3
3
  export declare class TokenRatesError extends Error {
4
4
  response?: Response;
5
5
  constructor(message: string, response?: Response);
6
6
  }
7
- export type CoingeckoConfig = {
7
+ export declare const ALL_CURRENCY_IDS: TokenRateCurrency[];
8
+ export type CoinsApiConfig = {
8
9
  apiUrl: string;
9
- apiKeyName?: string;
10
- apiKeyValue?: string;
11
10
  };
12
- export declare const DEFAULT_COINGECKO_CONFIG: CoingeckoConfig;
13
- export declare function fetchTokenRates(tokens: Record<TokenId, Token>, config?: CoingeckoConfig): Promise<TokenRatesList>;
11
+ export declare const DEFAULT_COINSAPI_CONFIG: CoinsApiConfig;
12
+ export declare function fetchTokenRates(tokens: Record<TokenId, Token>, currencyIds?: TokenRateCurrency[], config?: CoinsApiConfig): Promise<TokenRatesList>;
@@ -135,19 +135,11 @@ class TokenRatesError extends Error {
135
135
  this.response = response;
136
136
  }
137
137
  }
138
-
139
- // every currency in this list will be fetched from coingecko
140
- // comment out unused currencies to save some bandwidth!
141
- const coingeckoCurrencies = Object.keys(SUPPORTED_CURRENCIES);
142
- // api returns a 414 error if the url is too long, max length is about 8100 characters
143
- // so use 7900 to be safe
144
- const MAX_COINGECKO_URL_LENGTH = 7900;
145
- const DEFAULT_COINGECKO_CONFIG = {
146
- apiUrl: "https://api.coingecko.com"
138
+ const ALL_CURRENCY_IDS = Object.keys(SUPPORTED_CURRENCIES);
139
+ const DEFAULT_COINSAPI_CONFIG = {
140
+ apiUrl: "https://coins.talisman.xyz"
147
141
  };
148
-
149
- // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
150
- async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
142
+ async function fetchTokenRates(tokens, currencyIds = ALL_CURRENCY_IDS, config = DEFAULT_COINSAPI_CONFIG) {
151
143
  // create a map from `coingeckoId` -> `tokenId` for each token
152
144
  const coingeckoIdToTokenIds = Object.values(tokens)
153
145
  // ignore testnet tokens
@@ -192,56 +184,18 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
192
184
 
193
185
  // skip network request if there is nothing for us to fetch
194
186
  if (coingeckoIds.length < 1) return {};
195
-
196
- // construct a coingecko request, sort args to help proxies with caching
197
-
198
- const currenciesSerialized = coingeckoCurrencies.sort().join(",");
199
- const safelyGetCoingeckoUrls = coingeckoIds => {
200
- const idsSerialized = coingeckoIds.join(",");
201
- const queryUrl = `${config.apiUrl}/api/v3/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}&include_market_cap=true&include_24hr_change=true`;
202
- if (queryUrl.length > MAX_COINGECKO_URL_LENGTH) {
203
- const half = Math.floor(coingeckoIds.length / 2);
204
- return [...safelyGetCoingeckoUrls(coingeckoIds.slice(0, half)), ...safelyGetCoingeckoUrls(coingeckoIds.slice(half))];
205
- }
206
- return [queryUrl];
207
- };
208
-
209
- // fetch the token prices from coingecko
210
- // the response should be in the format:
211
- // {
212
- // [coingeckoId]: {
213
- // [currency]: rate
214
- // [currency_24h_change]: percent
215
- // [currency_market_cap]: value
216
- // }
217
- // }
218
-
219
- const coingeckoHeaders = new Headers();
220
- if (config.apiKeyName && config.apiKeyValue) {
221
- coingeckoHeaders.set(config.apiKeyName, config.apiKeyValue);
222
- }
223
- const coingeckoPrices = await Promise.all(safelyGetCoingeckoUrls(coingeckoIds).map(async queryUrl => await fetch(queryUrl, {
224
- headers: coingeckoHeaders
225
- }).then(response => {
226
- if (response.status !== 200) throw new TokenRatesError(`Failed to fetch token rates`, response);
227
- return response.json();
228
- }))).then(responses => Object.assign({}, ...responses));
187
+ const response = await fetch(`${config.apiUrl}/token-rates`, {
188
+ method: "POST",
189
+ body: JSON.stringify({
190
+ coingeckoIds,
191
+ currencyIds
192
+ })
193
+ });
194
+ const rawTokenRates = await response.json();
195
+ const tokenRates = parseTokenRatesFromApi(rawTokenRates, coingeckoIds, currencyIds);
229
196
 
230
197
  // build a TokenRatesList from the token prices result
231
- const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
232
- const tokenIds = coingeckoIdToTokenIds[coingeckoId];
233
- const rates = newTokenRates();
234
- for (const currency of coingeckoCurrencies) {
235
- if (coingeckoPrices[coingeckoId]?.[currency]) rates[currency] = {
236
- price: coingeckoPrices[coingeckoId][currency],
237
- marketCap: coingeckoPrices[coingeckoId][`${currency}_market_cap`],
238
- change24h: coingeckoPrices[coingeckoId][`${currency}_24h_change`]
239
- };
240
- }
241
- return tokenIds.map(tokenId => [tokenId, rates]);
242
- }));
243
-
244
- // return the TokenRatesList
198
+ const ratesList = Object.fromEntries(Object.entries(tokens).map(([tokenId, token]) => [tokenId, token.coingeckoId ? tokenRates[token.coingeckoId] ?? null : null]));
245
199
  return ratesList;
246
200
  }
247
201
 
@@ -249,7 +203,28 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
249
203
  // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
250
204
  const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
251
205
 
252
- exports.DEFAULT_COINGECKO_CONFIG = DEFAULT_COINGECKO_CONFIG;
206
+ // To save on bandwidth and work around response size limits, values are returned without json property names
207
+ // (e.g. [[[12, 12332, 0.5]]] instead of { dot : {usd: { value: 12, marketCap: 12332, change24h: 0.5 }} })
208
+
209
+ const parseTokenRatesFromApi = (rawTokenRates, coingeckoIds, currencyIds) => {
210
+ return Object.fromEntries(coingeckoIds.map((coingeckoId, idx) => {
211
+ const rates = rawTokenRates[idx];
212
+ if (!rates) return [coingeckoId, null];
213
+ return [coingeckoId, Object.fromEntries(currencyIds.map((currencyId, idx) => {
214
+ const curRate = rates[idx];
215
+ if (!curRate) return [currencyId, null];
216
+ const [price, marketCap, change24h] = rates[idx];
217
+ return [currencyId, {
218
+ price,
219
+ marketCap,
220
+ change24h
221
+ }];
222
+ }))];
223
+ }));
224
+ };
225
+
226
+ exports.ALL_CURRENCY_IDS = ALL_CURRENCY_IDS;
227
+ exports.DEFAULT_COINSAPI_CONFIG = DEFAULT_COINSAPI_CONFIG;
253
228
  exports.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
254
229
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
230
  exports.TokenRatesError = TokenRatesError;
@@ -135,19 +135,11 @@ class TokenRatesError extends Error {
135
135
  this.response = response;
136
136
  }
137
137
  }
138
-
139
- // every currency in this list will be fetched from coingecko
140
- // comment out unused currencies to save some bandwidth!
141
- const coingeckoCurrencies = Object.keys(SUPPORTED_CURRENCIES);
142
- // api returns a 414 error if the url is too long, max length is about 8100 characters
143
- // so use 7900 to be safe
144
- const MAX_COINGECKO_URL_LENGTH = 7900;
145
- const DEFAULT_COINGECKO_CONFIG = {
146
- apiUrl: "https://api.coingecko.com"
138
+ const ALL_CURRENCY_IDS = Object.keys(SUPPORTED_CURRENCIES);
139
+ const DEFAULT_COINSAPI_CONFIG = {
140
+ apiUrl: "https://coins.talisman.xyz"
147
141
  };
148
-
149
- // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
150
- async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
142
+ async function fetchTokenRates(tokens, currencyIds = ALL_CURRENCY_IDS, config = DEFAULT_COINSAPI_CONFIG) {
151
143
  // create a map from `coingeckoId` -> `tokenId` for each token
152
144
  const coingeckoIdToTokenIds = Object.values(tokens)
153
145
  // ignore testnet tokens
@@ -192,56 +184,18 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
192
184
 
193
185
  // skip network request if there is nothing for us to fetch
194
186
  if (coingeckoIds.length < 1) return {};
195
-
196
- // construct a coingecko request, sort args to help proxies with caching
197
-
198
- const currenciesSerialized = coingeckoCurrencies.sort().join(",");
199
- const safelyGetCoingeckoUrls = coingeckoIds => {
200
- const idsSerialized = coingeckoIds.join(",");
201
- const queryUrl = `${config.apiUrl}/api/v3/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}&include_market_cap=true&include_24hr_change=true`;
202
- if (queryUrl.length > MAX_COINGECKO_URL_LENGTH) {
203
- const half = Math.floor(coingeckoIds.length / 2);
204
- return [...safelyGetCoingeckoUrls(coingeckoIds.slice(0, half)), ...safelyGetCoingeckoUrls(coingeckoIds.slice(half))];
205
- }
206
- return [queryUrl];
207
- };
208
-
209
- // fetch the token prices from coingecko
210
- // the response should be in the format:
211
- // {
212
- // [coingeckoId]: {
213
- // [currency]: rate
214
- // [currency_24h_change]: percent
215
- // [currency_market_cap]: value
216
- // }
217
- // }
218
-
219
- const coingeckoHeaders = new Headers();
220
- if (config.apiKeyName && config.apiKeyValue) {
221
- coingeckoHeaders.set(config.apiKeyName, config.apiKeyValue);
222
- }
223
- const coingeckoPrices = await Promise.all(safelyGetCoingeckoUrls(coingeckoIds).map(async queryUrl => await fetch(queryUrl, {
224
- headers: coingeckoHeaders
225
- }).then(response => {
226
- if (response.status !== 200) throw new TokenRatesError(`Failed to fetch token rates`, response);
227
- return response.json();
228
- }))).then(responses => Object.assign({}, ...responses));
187
+ const response = await fetch(`${config.apiUrl}/token-rates`, {
188
+ method: "POST",
189
+ body: JSON.stringify({
190
+ coingeckoIds,
191
+ currencyIds
192
+ })
193
+ });
194
+ const rawTokenRates = await response.json();
195
+ const tokenRates = parseTokenRatesFromApi(rawTokenRates, coingeckoIds, currencyIds);
229
196
 
230
197
  // build a TokenRatesList from the token prices result
231
- const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
232
- const tokenIds = coingeckoIdToTokenIds[coingeckoId];
233
- const rates = newTokenRates();
234
- for (const currency of coingeckoCurrencies) {
235
- if (coingeckoPrices[coingeckoId]?.[currency]) rates[currency] = {
236
- price: coingeckoPrices[coingeckoId][currency],
237
- marketCap: coingeckoPrices[coingeckoId][`${currency}_market_cap`],
238
- change24h: coingeckoPrices[coingeckoId][`${currency}_24h_change`]
239
- };
240
- }
241
- return tokenIds.map(tokenId => [tokenId, rates]);
242
- }));
243
-
244
- // return the TokenRatesList
198
+ const ratesList = Object.fromEntries(Object.entries(tokens).map(([tokenId, token]) => [tokenId, token.coingeckoId ? tokenRates[token.coingeckoId] ?? null : null]));
245
199
  return ratesList;
246
200
  }
247
201
 
@@ -249,7 +203,28 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
249
203
  // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
250
204
  const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
251
205
 
252
- exports.DEFAULT_COINGECKO_CONFIG = DEFAULT_COINGECKO_CONFIG;
206
+ // To save on bandwidth and work around response size limits, values are returned without json property names
207
+ // (e.g. [[[12, 12332, 0.5]]] instead of { dot : {usd: { value: 12, marketCap: 12332, change24h: 0.5 }} })
208
+
209
+ const parseTokenRatesFromApi = (rawTokenRates, coingeckoIds, currencyIds) => {
210
+ return Object.fromEntries(coingeckoIds.map((coingeckoId, idx) => {
211
+ const rates = rawTokenRates[idx];
212
+ if (!rates) return [coingeckoId, null];
213
+ return [coingeckoId, Object.fromEntries(currencyIds.map((currencyId, idx) => {
214
+ const curRate = rates[idx];
215
+ if (!curRate) return [currencyId, null];
216
+ const [price, marketCap, change24h] = rates[idx];
217
+ return [currencyId, {
218
+ price,
219
+ marketCap,
220
+ change24h
221
+ }];
222
+ }))];
223
+ }));
224
+ };
225
+
226
+ exports.ALL_CURRENCY_IDS = ALL_CURRENCY_IDS;
227
+ exports.DEFAULT_COINSAPI_CONFIG = DEFAULT_COINSAPI_CONFIG;
253
228
  exports.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
254
229
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
230
  exports.TokenRatesError = TokenRatesError;
@@ -133,19 +133,11 @@ class TokenRatesError extends Error {
133
133
  this.response = response;
134
134
  }
135
135
  }
136
-
137
- // every currency in this list will be fetched from coingecko
138
- // comment out unused currencies to save some bandwidth!
139
- const coingeckoCurrencies = Object.keys(SUPPORTED_CURRENCIES);
140
- // api returns a 414 error if the url is too long, max length is about 8100 characters
141
- // so use 7900 to be safe
142
- const MAX_COINGECKO_URL_LENGTH = 7900;
143
- const DEFAULT_COINGECKO_CONFIG = {
144
- apiUrl: "https://api.coingecko.com"
136
+ const ALL_CURRENCY_IDS = Object.keys(SUPPORTED_CURRENCIES);
137
+ const DEFAULT_COINSAPI_CONFIG = {
138
+ apiUrl: "https://coins.talisman.xyz"
145
139
  };
146
-
147
- // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
148
- async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
140
+ async function fetchTokenRates(tokens, currencyIds = ALL_CURRENCY_IDS, config = DEFAULT_COINSAPI_CONFIG) {
149
141
  // create a map from `coingeckoId` -> `tokenId` for each token
150
142
  const coingeckoIdToTokenIds = Object.values(tokens)
151
143
  // ignore testnet tokens
@@ -190,56 +182,18 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
190
182
 
191
183
  // skip network request if there is nothing for us to fetch
192
184
  if (coingeckoIds.length < 1) return {};
193
-
194
- // construct a coingecko request, sort args to help proxies with caching
195
-
196
- const currenciesSerialized = coingeckoCurrencies.sort().join(",");
197
- const safelyGetCoingeckoUrls = coingeckoIds => {
198
- const idsSerialized = coingeckoIds.join(",");
199
- const queryUrl = `${config.apiUrl}/api/v3/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}&include_market_cap=true&include_24hr_change=true`;
200
- if (queryUrl.length > MAX_COINGECKO_URL_LENGTH) {
201
- const half = Math.floor(coingeckoIds.length / 2);
202
- return [...safelyGetCoingeckoUrls(coingeckoIds.slice(0, half)), ...safelyGetCoingeckoUrls(coingeckoIds.slice(half))];
203
- }
204
- return [queryUrl];
205
- };
206
-
207
- // fetch the token prices from coingecko
208
- // the response should be in the format:
209
- // {
210
- // [coingeckoId]: {
211
- // [currency]: rate
212
- // [currency_24h_change]: percent
213
- // [currency_market_cap]: value
214
- // }
215
- // }
216
-
217
- const coingeckoHeaders = new Headers();
218
- if (config.apiKeyName && config.apiKeyValue) {
219
- coingeckoHeaders.set(config.apiKeyName, config.apiKeyValue);
220
- }
221
- const coingeckoPrices = await Promise.all(safelyGetCoingeckoUrls(coingeckoIds).map(async queryUrl => await fetch(queryUrl, {
222
- headers: coingeckoHeaders
223
- }).then(response => {
224
- if (response.status !== 200) throw new TokenRatesError(`Failed to fetch token rates`, response);
225
- return response.json();
226
- }))).then(responses => Object.assign({}, ...responses));
185
+ const response = await fetch(`${config.apiUrl}/token-rates`, {
186
+ method: "POST",
187
+ body: JSON.stringify({
188
+ coingeckoIds,
189
+ currencyIds
190
+ })
191
+ });
192
+ const rawTokenRates = await response.json();
193
+ const tokenRates = parseTokenRatesFromApi(rawTokenRates, coingeckoIds, currencyIds);
227
194
 
228
195
  // build a TokenRatesList from the token prices result
229
- const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
230
- const tokenIds = coingeckoIdToTokenIds[coingeckoId];
231
- const rates = newTokenRates();
232
- for (const currency of coingeckoCurrencies) {
233
- if (coingeckoPrices[coingeckoId]?.[currency]) rates[currency] = {
234
- price: coingeckoPrices[coingeckoId][currency],
235
- marketCap: coingeckoPrices[coingeckoId][`${currency}_market_cap`],
236
- change24h: coingeckoPrices[coingeckoId][`${currency}_24h_change`]
237
- };
238
- }
239
- return tokenIds.map(tokenId => [tokenId, rates]);
240
- }));
241
-
242
- // return the TokenRatesList
196
+ const ratesList = Object.fromEntries(Object.entries(tokens).map(([tokenId, token]) => [tokenId, token.coingeckoId ? tokenRates[token.coingeckoId] ?? null : null]));
243
197
  return ratesList;
244
198
  }
245
199
 
@@ -247,4 +201,24 @@ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
247
201
  // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
248
202
  const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
249
203
 
250
- export { DEFAULT_COINGECKO_CONFIG, SUPPORTED_CURRENCIES, TalismanTokenRatesDatabase, TokenRatesError, db, fetchTokenRates, newTokenRates };
204
+ // To save on bandwidth and work around response size limits, values are returned without json property names
205
+ // (e.g. [[[12, 12332, 0.5]]] instead of { dot : {usd: { value: 12, marketCap: 12332, change24h: 0.5 }} })
206
+
207
+ const parseTokenRatesFromApi = (rawTokenRates, coingeckoIds, currencyIds) => {
208
+ return Object.fromEntries(coingeckoIds.map((coingeckoId, idx) => {
209
+ const rates = rawTokenRates[idx];
210
+ if (!rates) return [coingeckoId, null];
211
+ return [coingeckoId, Object.fromEntries(currencyIds.map((currencyId, idx) => {
212
+ const curRate = rates[idx];
213
+ if (!curRate) return [currencyId, null];
214
+ const [price, marketCap, change24h] = rates[idx];
215
+ return [currencyId, {
216
+ price,
217
+ marketCap,
218
+ change24h
219
+ }];
220
+ }))];
221
+ }));
222
+ };
223
+
224
+ export { ALL_CURRENCY_IDS, DEFAULT_COINSAPI_CONFIG, SUPPORTED_CURRENCIES, TalismanTokenRatesDatabase, TokenRatesError, db, fetchTokenRates, newTokenRates };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/token-rates",
3
- "version": "1.0.4",
3
+ "version": "2.0.1",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "dexie": "^4.0.9",
25
- "@talismn/chaindata-provider": "0.8.4"
25
+ "@talismn/chaindata-provider": "0.10.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/jest": "^29.5.14",
@@ -30,8 +30,8 @@
30
30
  "jest": "^29.7.0",
31
31
  "ts-jest": "^29.2.5",
32
32
  "typescript": "^5.6.3",
33
- "@talismn/eslint-config": "0.0.3",
34
- "@talismn/tsconfig": "0.0.2"
33
+ "@talismn/tsconfig": "0.0.2",
34
+ "@talismn/eslint-config": "0.0.3"
35
35
  },
36
36
  "eslintConfig": {
37
37
  "root": true,