@talismn/token-rates 0.3.0 → 1.0.0

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,7 +1,13 @@
1
- import { IToken, TokenId } from "@talismn/chaindata-provider";
1
+ import { Token, TokenId } from "@talismn/chaindata-provider";
2
2
  import { TokenRatesList } from "./types";
3
- export declare function fetchTokenRates(tokens: Record<TokenId, IToken>): Promise<TokenRatesList>;
4
- export interface WithCoingeckoId {
5
- coingeckoId: string;
3
+ export declare class TokenRatesError extends Error {
4
+ response?: Response;
5
+ constructor(message: string, response?: Response);
6
6
  }
7
- export declare function hasCoingeckoId(token: IToken): token is IToken & WithCoingeckoId;
7
+ export type CoingeckoConfig = {
8
+ apiUrl: string;
9
+ apiKeyName?: string;
10
+ apiKeyValue?: string;
11
+ };
12
+ export declare const DEFAULT_COINGECKO_CONFIG: CoingeckoConfig;
13
+ export declare function fetchTokenRates(tokens: Record<TokenId, Token>, config?: CoingeckoConfig): Promise<TokenRatesList>;
@@ -1,38 +1,96 @@
1
1
  import { TokenId } from "@talismn/chaindata-provider";
2
+ export declare const SUPPORTED_CURRENCIES: {
3
+ readonly btc: {
4
+ readonly name: "Bitcoin";
5
+ readonly symbol: "₿";
6
+ };
7
+ readonly eth: {
8
+ readonly name: "Ethereum";
9
+ readonly symbol: "Ξ";
10
+ };
11
+ readonly dot: {
12
+ readonly name: "Polkadot";
13
+ readonly symbol: "D";
14
+ };
15
+ readonly usd: {
16
+ readonly name: "US Dollar";
17
+ readonly symbol: "$";
18
+ };
19
+ readonly cny: {
20
+ readonly name: "Chinese Yuan";
21
+ readonly symbol: "¥";
22
+ };
23
+ readonly eur: {
24
+ readonly name: "Euro";
25
+ readonly symbol: "€";
26
+ };
27
+ readonly gbp: {
28
+ readonly name: "British Pound";
29
+ readonly symbol: "£";
30
+ };
31
+ readonly cad: {
32
+ readonly name: "Canadian Dollar";
33
+ readonly symbol: "C$";
34
+ };
35
+ readonly aud: {
36
+ readonly name: "Australian Dollar";
37
+ readonly symbol: "A$";
38
+ };
39
+ readonly nzd: {
40
+ readonly name: "New Zealand Dollar";
41
+ readonly symbol: "NZ$";
42
+ };
43
+ readonly jpy: {
44
+ readonly name: "Japanese Yen";
45
+ readonly symbol: "¥";
46
+ };
47
+ readonly rub: {
48
+ readonly name: "Russian Ruble";
49
+ readonly symbol: "₽";
50
+ };
51
+ readonly krw: {
52
+ readonly name: "South Korean Won";
53
+ readonly symbol: "₩";
54
+ };
55
+ readonly idr: {
56
+ readonly name: "Indonesian Rupiah";
57
+ readonly symbol: "Rp";
58
+ };
59
+ readonly php: {
60
+ readonly name: "Philippine Peso";
61
+ readonly symbol: "₱";
62
+ };
63
+ readonly thb: {
64
+ readonly name: "Thai Baht";
65
+ readonly symbol: "฿";
66
+ };
67
+ readonly vnd: {
68
+ readonly name: "Vietnamese Dong";
69
+ readonly symbol: "₫";
70
+ };
71
+ readonly inr: {
72
+ readonly name: "Indian Rupee";
73
+ readonly symbol: "₹";
74
+ };
75
+ readonly try: {
76
+ readonly name: "Turkish Lira";
77
+ readonly symbol: "₺";
78
+ };
79
+ readonly sgd: {
80
+ readonly name: "Singapore Dollar";
81
+ readonly symbol: "S$";
82
+ };
83
+ };
84
+ export type TokenRateCurrency = keyof typeof SUPPORTED_CURRENCIES;
85
+ export type TokenRateData = {
86
+ price: number;
87
+ marketCap?: number;
88
+ change24h?: number;
89
+ };
90
+ export type TokenRates = Record<TokenRateCurrency, TokenRateData | null>;
2
91
  export type TokenRatesList = Record<TokenId, TokenRates>;
3
- export type TokenRateCurrency = keyof TokenRates;
4
92
  export type DbTokenRates = {
5
93
  tokenId: TokenId;
6
94
  rates: TokenRates;
7
95
  };
8
- export type TokenRates = {
9
- /** us dollar rate */
10
- usd: number | null;
11
- /** australian dollar rate */
12
- aud: number | null;
13
- /** new zealand dollar rate */
14
- nzd: number | null;
15
- /** canadian dollar rate */
16
- cud: number | null;
17
- /** hong kong dollar rate */
18
- hkd: number | null;
19
- /** euro rate */
20
- eur: number | null;
21
- /** british pound sterling rate */
22
- gbp: number | null;
23
- /** japanese yen rate */
24
- jpy: number | null;
25
- /** south korean won rate */
26
- krw: number | null;
27
- /** chinese yuan rate */
28
- cny: number | null;
29
- /** russian yuan rate */
30
- rub: number | null;
31
- /** btc rate */
32
- btc: number | null;
33
- /** eth rate */
34
- eth: number | null;
35
- /** dot rate */
36
- dot: number | null;
37
- };
38
- export declare const NewTokenRates: () => TokenRates;
96
+ export declare const newTokenRates: () => TokenRates;
@@ -1,13 +1,6 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var dexie = require('dexie');
6
- var axios = require('axios');
7
-
8
- function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
9
-
10
- var axios__default = /*#__PURE__*/_interopDefault(axios);
11
4
 
12
5
  class TalismanTokenRatesDatabase extends dexie.Dexie {
13
6
  constructor() {
@@ -23,54 +16,166 @@ class TalismanTokenRatesDatabase extends dexie.Dexie {
23
16
  // https://dexie.org/docs/Version/Version.stores()#warning
24
17
  tokenRates: "tokenId"
25
18
  });
26
-
27
- // this.on("ready", async () => {})
28
19
  }
29
20
  }
30
-
31
21
  const db = new TalismanTokenRatesDatabase();
32
22
 
33
- const NewTokenRates = () => ({
23
+ const SUPPORTED_CURRENCIES = {
24
+ btc: {
25
+ name: "Bitcoin",
26
+ symbol: "₿"
27
+ },
28
+ eth: {
29
+ name: "Ethereum",
30
+ symbol: "Ξ"
31
+ },
32
+ dot: {
33
+ name: "Polkadot",
34
+ symbol: "D"
35
+ },
36
+ usd: {
37
+ name: "US Dollar",
38
+ symbol: "$"
39
+ },
40
+ cny: {
41
+ name: "Chinese Yuan",
42
+ symbol: "¥"
43
+ },
44
+ eur: {
45
+ name: "Euro",
46
+ symbol: "€"
47
+ },
48
+ gbp: {
49
+ name: "British Pound",
50
+ symbol: "£"
51
+ },
52
+ cad: {
53
+ name: "Canadian Dollar",
54
+ symbol: "C$"
55
+ },
56
+ aud: {
57
+ name: "Australian Dollar",
58
+ symbol: "A$"
59
+ },
60
+ nzd: {
61
+ name: "New Zealand Dollar",
62
+ symbol: "NZ$"
63
+ },
64
+ jpy: {
65
+ name: "Japanese Yen",
66
+ symbol: "¥"
67
+ },
68
+ rub: {
69
+ name: "Russian Ruble",
70
+ symbol: "₽"
71
+ },
72
+ krw: {
73
+ name: "South Korean Won",
74
+ symbol: "₩"
75
+ },
76
+ idr: {
77
+ name: "Indonesian Rupiah",
78
+ symbol: "Rp"
79
+ },
80
+ php: {
81
+ name: "Philippine Peso",
82
+ symbol: "₱"
83
+ },
84
+ thb: {
85
+ name: "Thai Baht",
86
+ symbol: "฿"
87
+ },
88
+ vnd: {
89
+ name: "Vietnamese Dong",
90
+ symbol: "₫"
91
+ },
92
+ inr: {
93
+ name: "Indian Rupee",
94
+ symbol: "₹"
95
+ },
96
+ try: {
97
+ name: "Turkish Lira",
98
+ symbol: "₺"
99
+ },
100
+ // hkd: { name: "Hong Kong Dollar", symbol: "HK$" },
101
+ sgd: {
102
+ name: "Singapore Dollar",
103
+ symbol: "S$"
104
+ }
105
+ // twd: { name: "Taiwanese Dollar", symbol: "NT$" },
106
+ };
107
+ const newTokenRates = () => ({
108
+ btc: null,
109
+ eth: null,
110
+ dot: null,
34
111
  usd: null,
35
- aud: null,
36
- nzd: null,
37
- cud: null,
38
- hkd: null,
112
+ cny: null,
39
113
  eur: null,
40
114
  gbp: null,
115
+ cad: null,
116
+ aud: null,
117
+ nzd: null,
41
118
  jpy: null,
42
- krw: null,
43
- cny: null,
44
119
  rub: null,
45
- btc: null,
46
- eth: null,
47
- dot: null
120
+ krw: null,
121
+ idr: null,
122
+ php: null,
123
+ thb: null,
124
+ vnd: null,
125
+ inr: null,
126
+ try: null,
127
+ // hkd: null,
128
+ sgd: null
129
+ // twd: null,
48
130
  });
49
131
 
50
- // the base url of the v3 coingecko api
51
- const coingeckoApiUrl = "https://api.coingecko.com/api/v3";
132
+ class TokenRatesError extends Error {
133
+ constructor(message, response) {
134
+ super(message);
135
+ this.response = response;
136
+ }
137
+ }
52
138
 
53
139
  // every currency in this list will be fetched from coingecko
54
140
  // comment out unused currencies to save some bandwidth!
55
- const coingeckoCurrencies = ["usd", "aud",
56
- // 'nzd',
57
- // 'cud',
58
- // 'hkd',
59
- "eur", "gbp", "jpy",
60
- // 'krw',
61
- "cny", "rub", "btc", "eth", "dot"];
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"
147
+ };
62
148
 
63
149
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
64
- async function fetchTokenRates(tokens) {
150
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
65
151
  // create a map from `coingeckoId` -> `tokenId` for each token
66
152
  const coingeckoIdToTokenIds = Object.values(tokens)
67
153
  // ignore testnet tokens
68
154
  .filter(({
69
155
  isTestnet
70
- }) => !isTestnet)
156
+ }) => !isTestnet).flatMap(token => {
157
+ // BEGIN: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
158
+ //
159
+ // This section contains the logic such that: if token is an LP token, then fetch the rates for the two underlying tokens.
160
+ if (token.type === "evm-uniswapv2") {
161
+ if (!token.evmNetwork) return [];
162
+ const getToken = (evmNetworkId, tokenAddress, coingeckoId) => ({
163
+ id: evmErc20TokenId(evmNetworkId, tokenAddress),
164
+ coingeckoId
165
+ });
166
+ const token0 = token.coingeckoId0 ? [getToken(token.evmNetwork.id, token.tokenAddress0, token.coingeckoId0)] : [];
167
+ const token1 = token.coingeckoId1 ? [getToken(token.evmNetwork.id, token.tokenAddress1, token.coingeckoId1)] : [];
168
+ return [...token0, ...token1];
169
+ }
170
+ // END: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
71
171
 
72
- // ignore tokens which don't have a coingeckoId
73
- .filter(hasCoingeckoId)
172
+ // ignore tokens which don't have a coingeckoId
173
+ if (!token.coingeckoId) return [];
174
+ return [{
175
+ id: token.id,
176
+ coingeckoId: token.coingeckoId
177
+ }];
178
+ })
74
179
 
75
180
  // get each token's coingeckoId
76
181
  .reduce((coingeckoIdToTokenIds, {
@@ -83,31 +188,55 @@ async function fetchTokenRates(tokens) {
83
188
  }, {});
84
189
 
85
190
  // create a list of coingeckoIds we want to fetch
86
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
191
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
87
192
 
88
193
  // skip network request if there is nothing for us to fetch
89
194
  if (coingeckoIds.length < 1) return {};
90
195
 
91
- // construct a coingecko request
92
- const idsSerialized = coingeckoIds.join(",");
93
- const currenciesSerialized = coingeckoCurrencies.join(",");
94
- const queryUrl = `${coingeckoApiUrl}/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}`;
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
+ };
95
208
 
96
209
  // fetch the token prices from coingecko
97
210
  // the response should be in the format:
98
211
  // {
99
212
  // [coingeckoId]: {
100
213
  // [currency]: rate
214
+ // [currency_24h_change]: percent
215
+ // [currency_market_cap]: value
101
216
  // }
102
217
  // }
103
- const coingeckoPrices = await axios__default["default"].get(queryUrl).then(response => response.data);
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));
104
229
 
105
230
  // build a TokenRatesList from the token prices result
106
231
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
107
232
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
108
- const rates = NewTokenRates();
233
+ const rates = newTokenRates();
109
234
  for (const currency of coingeckoCurrencies) {
110
- rates[currency] = ((coingeckoPrices || {})[coingeckoId] || {})[currency] || null;
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
+ };
111
240
  }
112
241
  return tokenIds.map(tokenId => [tokenId, rates]);
113
242
  }));
@@ -115,12 +244,15 @@ async function fetchTokenRates(tokens) {
115
244
  // return the TokenRatesList
116
245
  return ratesList;
117
246
  }
118
- function hasCoingeckoId(token) {
119
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
120
- }
121
247
 
122
- exports.NewTokenRates = NewTokenRates;
248
+ // TODO: Move this into a common module which can then be imported both here and into EvmErc20Module
249
+ // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
250
+ const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
251
+
252
+ exports.DEFAULT_COINGECKO_CONFIG = DEFAULT_COINGECKO_CONFIG;
253
+ exports.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
123
254
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
+ exports.TokenRatesError = TokenRatesError;
124
256
  exports.db = db;
125
257
  exports.fetchTokenRates = fetchTokenRates;
126
- exports.hasCoingeckoId = hasCoingeckoId;
258
+ exports.newTokenRates = newTokenRates;
@@ -1,13 +1,6 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var dexie = require('dexie');
6
- var axios = require('axios');
7
-
8
- function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
9
-
10
- var axios__default = /*#__PURE__*/_interopDefault(axios);
11
4
 
12
5
  class TalismanTokenRatesDatabase extends dexie.Dexie {
13
6
  constructor() {
@@ -23,54 +16,166 @@ class TalismanTokenRatesDatabase extends dexie.Dexie {
23
16
  // https://dexie.org/docs/Version/Version.stores()#warning
24
17
  tokenRates: "tokenId"
25
18
  });
26
-
27
- // this.on("ready", async () => {})
28
19
  }
29
20
  }
30
-
31
21
  const db = new TalismanTokenRatesDatabase();
32
22
 
33
- const NewTokenRates = () => ({
23
+ const SUPPORTED_CURRENCIES = {
24
+ btc: {
25
+ name: "Bitcoin",
26
+ symbol: "₿"
27
+ },
28
+ eth: {
29
+ name: "Ethereum",
30
+ symbol: "Ξ"
31
+ },
32
+ dot: {
33
+ name: "Polkadot",
34
+ symbol: "D"
35
+ },
36
+ usd: {
37
+ name: "US Dollar",
38
+ symbol: "$"
39
+ },
40
+ cny: {
41
+ name: "Chinese Yuan",
42
+ symbol: "¥"
43
+ },
44
+ eur: {
45
+ name: "Euro",
46
+ symbol: "€"
47
+ },
48
+ gbp: {
49
+ name: "British Pound",
50
+ symbol: "£"
51
+ },
52
+ cad: {
53
+ name: "Canadian Dollar",
54
+ symbol: "C$"
55
+ },
56
+ aud: {
57
+ name: "Australian Dollar",
58
+ symbol: "A$"
59
+ },
60
+ nzd: {
61
+ name: "New Zealand Dollar",
62
+ symbol: "NZ$"
63
+ },
64
+ jpy: {
65
+ name: "Japanese Yen",
66
+ symbol: "¥"
67
+ },
68
+ rub: {
69
+ name: "Russian Ruble",
70
+ symbol: "₽"
71
+ },
72
+ krw: {
73
+ name: "South Korean Won",
74
+ symbol: "₩"
75
+ },
76
+ idr: {
77
+ name: "Indonesian Rupiah",
78
+ symbol: "Rp"
79
+ },
80
+ php: {
81
+ name: "Philippine Peso",
82
+ symbol: "₱"
83
+ },
84
+ thb: {
85
+ name: "Thai Baht",
86
+ symbol: "฿"
87
+ },
88
+ vnd: {
89
+ name: "Vietnamese Dong",
90
+ symbol: "₫"
91
+ },
92
+ inr: {
93
+ name: "Indian Rupee",
94
+ symbol: "₹"
95
+ },
96
+ try: {
97
+ name: "Turkish Lira",
98
+ symbol: "₺"
99
+ },
100
+ // hkd: { name: "Hong Kong Dollar", symbol: "HK$" },
101
+ sgd: {
102
+ name: "Singapore Dollar",
103
+ symbol: "S$"
104
+ }
105
+ // twd: { name: "Taiwanese Dollar", symbol: "NT$" },
106
+ };
107
+ const newTokenRates = () => ({
108
+ btc: null,
109
+ eth: null,
110
+ dot: null,
34
111
  usd: null,
35
- aud: null,
36
- nzd: null,
37
- cud: null,
38
- hkd: null,
112
+ cny: null,
39
113
  eur: null,
40
114
  gbp: null,
115
+ cad: null,
116
+ aud: null,
117
+ nzd: null,
41
118
  jpy: null,
42
- krw: null,
43
- cny: null,
44
119
  rub: null,
45
- btc: null,
46
- eth: null,
47
- dot: null
120
+ krw: null,
121
+ idr: null,
122
+ php: null,
123
+ thb: null,
124
+ vnd: null,
125
+ inr: null,
126
+ try: null,
127
+ // hkd: null,
128
+ sgd: null
129
+ // twd: null,
48
130
  });
49
131
 
50
- // the base url of the v3 coingecko api
51
- const coingeckoApiUrl = "https://api.coingecko.com/api/v3";
132
+ class TokenRatesError extends Error {
133
+ constructor(message, response) {
134
+ super(message);
135
+ this.response = response;
136
+ }
137
+ }
52
138
 
53
139
  // every currency in this list will be fetched from coingecko
54
140
  // comment out unused currencies to save some bandwidth!
55
- const coingeckoCurrencies = ["usd", "aud",
56
- // 'nzd',
57
- // 'cud',
58
- // 'hkd',
59
- "eur", "gbp", "jpy",
60
- // 'krw',
61
- "cny", "rub", "btc", "eth", "dot"];
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"
147
+ };
62
148
 
63
149
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
64
- async function fetchTokenRates(tokens) {
150
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
65
151
  // create a map from `coingeckoId` -> `tokenId` for each token
66
152
  const coingeckoIdToTokenIds = Object.values(tokens)
67
153
  // ignore testnet tokens
68
154
  .filter(({
69
155
  isTestnet
70
- }) => !isTestnet)
156
+ }) => !isTestnet).flatMap(token => {
157
+ // BEGIN: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
158
+ //
159
+ // This section contains the logic such that: if token is an LP token, then fetch the rates for the two underlying tokens.
160
+ if (token.type === "evm-uniswapv2") {
161
+ if (!token.evmNetwork) return [];
162
+ const getToken = (evmNetworkId, tokenAddress, coingeckoId) => ({
163
+ id: evmErc20TokenId(evmNetworkId, tokenAddress),
164
+ coingeckoId
165
+ });
166
+ const token0 = token.coingeckoId0 ? [getToken(token.evmNetwork.id, token.tokenAddress0, token.coingeckoId0)] : [];
167
+ const token1 = token.coingeckoId1 ? [getToken(token.evmNetwork.id, token.tokenAddress1, token.coingeckoId1)] : [];
168
+ return [...token0, ...token1];
169
+ }
170
+ // END: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
71
171
 
72
- // ignore tokens which don't have a coingeckoId
73
- .filter(hasCoingeckoId)
172
+ // ignore tokens which don't have a coingeckoId
173
+ if (!token.coingeckoId) return [];
174
+ return [{
175
+ id: token.id,
176
+ coingeckoId: token.coingeckoId
177
+ }];
178
+ })
74
179
 
75
180
  // get each token's coingeckoId
76
181
  .reduce((coingeckoIdToTokenIds, {
@@ -83,31 +188,55 @@ async function fetchTokenRates(tokens) {
83
188
  }, {});
84
189
 
85
190
  // create a list of coingeckoIds we want to fetch
86
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
191
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
87
192
 
88
193
  // skip network request if there is nothing for us to fetch
89
194
  if (coingeckoIds.length < 1) return {};
90
195
 
91
- // construct a coingecko request
92
- const idsSerialized = coingeckoIds.join(",");
93
- const currenciesSerialized = coingeckoCurrencies.join(",");
94
- const queryUrl = `${coingeckoApiUrl}/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}`;
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
+ };
95
208
 
96
209
  // fetch the token prices from coingecko
97
210
  // the response should be in the format:
98
211
  // {
99
212
  // [coingeckoId]: {
100
213
  // [currency]: rate
214
+ // [currency_24h_change]: percent
215
+ // [currency_market_cap]: value
101
216
  // }
102
217
  // }
103
- const coingeckoPrices = await axios__default["default"].get(queryUrl).then(response => response.data);
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));
104
229
 
105
230
  // build a TokenRatesList from the token prices result
106
231
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
107
232
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
108
- const rates = NewTokenRates();
233
+ const rates = newTokenRates();
109
234
  for (const currency of coingeckoCurrencies) {
110
- rates[currency] = ((coingeckoPrices || {})[coingeckoId] || {})[currency] || null;
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
+ };
111
240
  }
112
241
  return tokenIds.map(tokenId => [tokenId, rates]);
113
242
  }));
@@ -115,12 +244,15 @@ async function fetchTokenRates(tokens) {
115
244
  // return the TokenRatesList
116
245
  return ratesList;
117
246
  }
118
- function hasCoingeckoId(token) {
119
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
120
- }
121
247
 
122
- exports.NewTokenRates = NewTokenRates;
248
+ // TODO: Move this into a common module which can then be imported both here and into EvmErc20Module
249
+ // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
250
+ const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
251
+
252
+ exports.DEFAULT_COINGECKO_CONFIG = DEFAULT_COINGECKO_CONFIG;
253
+ exports.SUPPORTED_CURRENCIES = SUPPORTED_CURRENCIES;
123
254
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
+ exports.TokenRatesError = TokenRatesError;
124
256
  exports.db = db;
125
257
  exports.fetchTokenRates = fetchTokenRates;
126
- exports.hasCoingeckoId = hasCoingeckoId;
258
+ exports.newTokenRates = newTokenRates;
@@ -1,5 +1,4 @@
1
1
  import { Dexie } from 'dexie';
2
- import axios from 'axios';
3
2
 
4
3
  class TalismanTokenRatesDatabase extends Dexie {
5
4
  constructor() {
@@ -15,54 +14,166 @@ class TalismanTokenRatesDatabase extends Dexie {
15
14
  // https://dexie.org/docs/Version/Version.stores()#warning
16
15
  tokenRates: "tokenId"
17
16
  });
18
-
19
- // this.on("ready", async () => {})
20
17
  }
21
18
  }
22
-
23
19
  const db = new TalismanTokenRatesDatabase();
24
20
 
25
- const NewTokenRates = () => ({
21
+ const SUPPORTED_CURRENCIES = {
22
+ btc: {
23
+ name: "Bitcoin",
24
+ symbol: "₿"
25
+ },
26
+ eth: {
27
+ name: "Ethereum",
28
+ symbol: "Ξ"
29
+ },
30
+ dot: {
31
+ name: "Polkadot",
32
+ symbol: "D"
33
+ },
34
+ usd: {
35
+ name: "US Dollar",
36
+ symbol: "$"
37
+ },
38
+ cny: {
39
+ name: "Chinese Yuan",
40
+ symbol: "¥"
41
+ },
42
+ eur: {
43
+ name: "Euro",
44
+ symbol: "€"
45
+ },
46
+ gbp: {
47
+ name: "British Pound",
48
+ symbol: "£"
49
+ },
50
+ cad: {
51
+ name: "Canadian Dollar",
52
+ symbol: "C$"
53
+ },
54
+ aud: {
55
+ name: "Australian Dollar",
56
+ symbol: "A$"
57
+ },
58
+ nzd: {
59
+ name: "New Zealand Dollar",
60
+ symbol: "NZ$"
61
+ },
62
+ jpy: {
63
+ name: "Japanese Yen",
64
+ symbol: "¥"
65
+ },
66
+ rub: {
67
+ name: "Russian Ruble",
68
+ symbol: "₽"
69
+ },
70
+ krw: {
71
+ name: "South Korean Won",
72
+ symbol: "₩"
73
+ },
74
+ idr: {
75
+ name: "Indonesian Rupiah",
76
+ symbol: "Rp"
77
+ },
78
+ php: {
79
+ name: "Philippine Peso",
80
+ symbol: "₱"
81
+ },
82
+ thb: {
83
+ name: "Thai Baht",
84
+ symbol: "฿"
85
+ },
86
+ vnd: {
87
+ name: "Vietnamese Dong",
88
+ symbol: "₫"
89
+ },
90
+ inr: {
91
+ name: "Indian Rupee",
92
+ symbol: "₹"
93
+ },
94
+ try: {
95
+ name: "Turkish Lira",
96
+ symbol: "₺"
97
+ },
98
+ // hkd: { name: "Hong Kong Dollar", symbol: "HK$" },
99
+ sgd: {
100
+ name: "Singapore Dollar",
101
+ symbol: "S$"
102
+ }
103
+ // twd: { name: "Taiwanese Dollar", symbol: "NT$" },
104
+ };
105
+ const newTokenRates = () => ({
106
+ btc: null,
107
+ eth: null,
108
+ dot: null,
26
109
  usd: null,
27
- aud: null,
28
- nzd: null,
29
- cud: null,
30
- hkd: null,
110
+ cny: null,
31
111
  eur: null,
32
112
  gbp: null,
113
+ cad: null,
114
+ aud: null,
115
+ nzd: null,
33
116
  jpy: null,
34
- krw: null,
35
- cny: null,
36
117
  rub: null,
37
- btc: null,
38
- eth: null,
39
- dot: null
118
+ krw: null,
119
+ idr: null,
120
+ php: null,
121
+ thb: null,
122
+ vnd: null,
123
+ inr: null,
124
+ try: null,
125
+ // hkd: null,
126
+ sgd: null
127
+ // twd: null,
40
128
  });
41
129
 
42
- // the base url of the v3 coingecko api
43
- const coingeckoApiUrl = "https://api.coingecko.com/api/v3";
130
+ class TokenRatesError extends Error {
131
+ constructor(message, response) {
132
+ super(message);
133
+ this.response = response;
134
+ }
135
+ }
44
136
 
45
137
  // every currency in this list will be fetched from coingecko
46
138
  // comment out unused currencies to save some bandwidth!
47
- const coingeckoCurrencies = ["usd", "aud",
48
- // 'nzd',
49
- // 'cud',
50
- // 'hkd',
51
- "eur", "gbp", "jpy",
52
- // 'krw',
53
- "cny", "rub", "btc", "eth", "dot"];
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"
145
+ };
54
146
 
55
147
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
56
- async function fetchTokenRates(tokens) {
148
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
57
149
  // create a map from `coingeckoId` -> `tokenId` for each token
58
150
  const coingeckoIdToTokenIds = Object.values(tokens)
59
151
  // ignore testnet tokens
60
152
  .filter(({
61
153
  isTestnet
62
- }) => !isTestnet)
154
+ }) => !isTestnet).flatMap(token => {
155
+ // BEGIN: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
156
+ //
157
+ // This section contains the logic such that: if token is an LP token, then fetch the rates for the two underlying tokens.
158
+ if (token.type === "evm-uniswapv2") {
159
+ if (!token.evmNetwork) return [];
160
+ const getToken = (evmNetworkId, tokenAddress, coingeckoId) => ({
161
+ id: evmErc20TokenId(evmNetworkId, tokenAddress),
162
+ coingeckoId
163
+ });
164
+ const token0 = token.coingeckoId0 ? [getToken(token.evmNetwork.id, token.tokenAddress0, token.coingeckoId0)] : [];
165
+ const token1 = token.coingeckoId1 ? [getToken(token.evmNetwork.id, token.tokenAddress1, token.coingeckoId1)] : [];
166
+ return [...token0, ...token1];
167
+ }
168
+ // END: LP tokens have a rate which is calculated later on, using the rates of two other tokens.
63
169
 
64
- // ignore tokens which don't have a coingeckoId
65
- .filter(hasCoingeckoId)
170
+ // ignore tokens which don't have a coingeckoId
171
+ if (!token.coingeckoId) return [];
172
+ return [{
173
+ id: token.id,
174
+ coingeckoId: token.coingeckoId
175
+ }];
176
+ })
66
177
 
67
178
  // get each token's coingeckoId
68
179
  .reduce((coingeckoIdToTokenIds, {
@@ -75,31 +186,55 @@ async function fetchTokenRates(tokens) {
75
186
  }, {});
76
187
 
77
188
  // create a list of coingeckoIds we want to fetch
78
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
189
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
79
190
 
80
191
  // skip network request if there is nothing for us to fetch
81
192
  if (coingeckoIds.length < 1) return {};
82
193
 
83
- // construct a coingecko request
84
- const idsSerialized = coingeckoIds.join(",");
85
- const currenciesSerialized = coingeckoCurrencies.join(",");
86
- const queryUrl = `${coingeckoApiUrl}/simple/price?ids=${idsSerialized}&vs_currencies=${currenciesSerialized}`;
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
+ };
87
206
 
88
207
  // fetch the token prices from coingecko
89
208
  // the response should be in the format:
90
209
  // {
91
210
  // [coingeckoId]: {
92
211
  // [currency]: rate
212
+ // [currency_24h_change]: percent
213
+ // [currency_market_cap]: value
93
214
  // }
94
215
  // }
95
- const coingeckoPrices = await axios.get(queryUrl).then(response => response.data);
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));
96
227
 
97
228
  // build a TokenRatesList from the token prices result
98
229
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
99
230
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
100
- const rates = NewTokenRates();
231
+ const rates = newTokenRates();
101
232
  for (const currency of coingeckoCurrencies) {
102
- rates[currency] = ((coingeckoPrices || {})[coingeckoId] || {})[currency] || null;
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
+ };
103
238
  }
104
239
  return tokenIds.map(tokenId => [tokenId, rates]);
105
240
  }));
@@ -107,8 +242,9 @@ async function fetchTokenRates(tokens) {
107
242
  // return the TokenRatesList
108
243
  return ratesList;
109
244
  }
110
- function hasCoingeckoId(token) {
111
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
112
- }
113
245
 
114
- export { NewTokenRates, TalismanTokenRatesDatabase, db, fetchTokenRates, hasCoingeckoId };
246
+ // TODO: Move this into a common module which can then be imported both here and into EvmErc20Module
247
+ // We can't import this directly from EvmErc20Module because this package doesn't depend on `@talismn/balances`
248
+ const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
249
+
250
+ export { DEFAULT_COINGECKO_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": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -20,29 +20,28 @@
20
20
  "engines": {
21
21
  "node": ">=18"
22
22
  },
23
- "scripts": {
24
- "test": "jest",
25
- "lint": "eslint src --max-warnings 0",
26
- "clean": "rm -rf dist && rm -rf .turbo rm -rf node_modules"
27
- },
28
23
  "dependencies": {
29
- "@talismn/chaindata-provider": "0.8.0",
30
- "axios": "^0.27.2",
31
- "dexie": "^3.2.4"
24
+ "dexie": "^4.0.9",
25
+ "@talismn/chaindata-provider": "0.8.0"
32
26
  },
33
27
  "devDependencies": {
34
- "@talismn/eslint-config": "0.0.3",
35
- "@talismn/tsconfig": "0.0.2",
36
- "@types/jest": "^27.5.1",
37
- "eslint": "^8.52.0",
28
+ "@types/jest": "^29.5.14",
29
+ "eslint": "^8.57.1",
38
30
  "jest": "^29.7.0",
39
- "ts-jest": "^29.1.1",
40
- "typescript": "^5.2.2"
31
+ "ts-jest": "^29.2.5",
32
+ "typescript": "^5.6.3",
33
+ "@talismn/tsconfig": "0.0.2",
34
+ "@talismn/eslint-config": "0.0.3"
41
35
  },
42
36
  "eslintConfig": {
43
37
  "root": true,
44
38
  "extends": [
45
39
  "@talismn/eslint-config/base"
46
40
  ]
41
+ },
42
+ "scripts": {
43
+ "test": "jest",
44
+ "lint": "eslint src --max-warnings 0",
45
+ "clean": "rm -rf dist .turbo node_modules"
47
46
  }
48
47
  }
package/CHANGELOG.md DELETED
@@ -1,178 +0,0 @@
1
- # @talismn/token-rates
2
-
3
- ## 0.3.0
4
-
5
- ### Minor Changes
6
-
7
- - 123647e: - Additional currencies support for token rate
8
- - New Dollar sign icon
9
- - 123647e: Add extra currency options
10
-
11
- ### Patch Changes
12
-
13
- - c4d5967: bump typescript version
14
- - 620b7eb: Dependency updates
15
- - Updated dependencies [03939d5]
16
- - Updated dependencies [ade2908]
17
- - Updated dependencies [c4d5967]
18
- - Updated dependencies [620b7eb]
19
- - Updated dependencies [5aadf99]
20
- - Updated dependencies [4cace80]
21
- - @talismn/chaindata-provider@0.8.0
22
-
23
- ## 0.2.0
24
-
25
- ### Minor Changes
26
-
27
- - b920ab98: Added GPL licence
28
-
29
- ### Patch Changes
30
-
31
- - 3c1a8b10: Dependency updates
32
- - Updated dependencies [3c1a8b10]
33
- - Updated dependencies [b920ab98]
34
- - @talismn/chaindata-provider@0.7.0
35
-
36
- ## 0.1.18
37
-
38
- ### Patch Changes
39
-
40
- - @talismn/chaindata-provider@0.6.0
41
-
42
- ## 0.1.17
43
-
44
- ### Patch Changes
45
-
46
- - Updated dependencies [1a2fdc73]
47
- - @talismn/chaindata-provider@0.5.0
48
-
49
- ## 0.1.16
50
-
51
- ### Patch Changes
52
-
53
- - f7aca48b: eslint rules
54
- - 01bf239b: fix: packages publishing with incorrect interdependency versions
55
- - Updated dependencies [f7aca48b]
56
- - Updated dependencies [48f0222e]
57
- - Updated dependencies [01bf239b]
58
- - @talismn/chaindata-provider@0.4.4
59
-
60
- ## 0.1.15
61
-
62
- ### Patch Changes
63
-
64
- - 6643a4e4: fix: tokenRates in @talismn/balances-react
65
- - Updated dependencies [79f6ccf6]
66
- - Updated dependencies [c24dc1fb]
67
- - @talismn/chaindata-provider@0.4.3
68
-
69
- ## 0.1.14
70
-
71
- ### Patch Changes
72
-
73
- - @talismn/chaindata-provider@0.4.2
74
-
75
- ## 0.1.13
76
-
77
- ### Patch Changes
78
-
79
- - 8adc7f06: feat: switched build tool to preconstruct
80
- - Updated dependencies [8adc7f06]
81
- - @talismn/chaindata-provider@0.4.1
82
-
83
- ## 0.1.12
84
-
85
- ### Patch Changes
86
-
87
- - 4aa691d: feat: new balance modules
88
- - Updated dependencies [4aa691d]
89
- - @talismn/chaindata-provider@0.2.1
90
-
91
- ## 0.1.11
92
-
93
- ### Patch Changes
94
-
95
- - @talismn/chaindata-provider@0.2.0
96
-
97
- ## 0.1.10
98
-
99
- ### Patch Changes
100
-
101
- - fix: a variety of improvements from the wallet integration
102
- - Updated dependencies
103
- - @talismn/chaindata-provider@0.1.10
104
-
105
- ## 0.1.9
106
-
107
- ### Patch Changes
108
-
109
- - Updated dependencies [8ecb8214]
110
- - @talismn/chaindata-provider@0.1.9
111
-
112
- ## 0.1.8
113
-
114
- ### Patch Changes
115
-
116
- - @talismn/chaindata-provider@0.1.8
117
-
118
- ## 0.1.7
119
-
120
- ### Patch Changes
121
-
122
- - db04d0d: fix: missing token rates and empty token rates requests
123
- - @talismn/chaindata-provider@0.1.7
124
-
125
- ## 0.1.6
126
-
127
- ### Patch Changes
128
-
129
- - ca50757: feat: implemented token fiat rates in @talismn/balances
130
- - Updated dependencies [ca50757]
131
- - @talismn/chaindata-provider@0.1.6
132
-
133
- ## 0.1.5
134
-
135
- ### Patch Changes
136
-
137
- - Updated dependencies [d66c5bc]
138
- - @talismn/chaindata-provider@0.1.5
139
-
140
- ## 0.1.4
141
-
142
- ### Patch Changes
143
-
144
- - @talismn/chaindata-provider@0.1.4
145
-
146
- ## 0.1.3
147
-
148
- ### Patch Changes
149
-
150
- - Updated dependencies [d5f69f7]
151
- - @talismn/chaindata-provider@0.1.3
152
-
153
- ## 0.1.2
154
-
155
- ### Patch Changes
156
-
157
- - 5af305c: switched build output from esm to commonjs for ecosystem compatibility
158
- - Updated dependencies [5af305c]
159
- - @talismn/chaindata-provider@0.1.2
160
-
161
- ## 0.1.1
162
-
163
- ### Patch Changes
164
-
165
- - Fixed publish config
166
- - Updated dependencies
167
- - @talismn/chaindata-provider@0.1.1
168
-
169
- ## 0.1.0
170
-
171
- ### Minor Changes
172
-
173
- - 43c1a3a: Initial release
174
-
175
- ### Patch Changes
176
-
177
- - Updated dependencies [43c1a3a]
178
- - @talismn/chaindata-provider@0.1.0