@talismn/token-rates 0.2.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,36 +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
- /** btc rate */
30
- btc: number | null;
31
- /** eth rate */
32
- eth: number | null;
33
- /** dot rate */
34
- dot: number | null;
35
- };
36
- 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,60 +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,
119
+ rub: null,
42
120
  krw: null,
43
- cny: null,
44
- btc: null,
45
- eth: null,
46
- dot: 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,
47
130
  });
48
131
 
49
- // the base url of the v3 coingecko api
50
- 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
+ }
51
138
 
52
139
  // every currency in this list will be fetched from coingecko
53
140
  // comment out unused currencies to save some bandwidth!
54
- const coingeckoCurrencies = ["usd",
55
- // 'aud',
56
- // 'nzd',
57
- // 'cud',
58
- // 'hkd',
59
- "eur"
60
- // 'gbp',
61
- // 'jpy',
62
- // 'krw',
63
- // 'cny',
64
- // 'btc',
65
- // 'eth',
66
- // 'dot',
67
- ];
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
+ };
68
148
 
69
149
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
70
- async function fetchTokenRates(tokens) {
150
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
71
151
  // create a map from `coingeckoId` -> `tokenId` for each token
72
152
  const coingeckoIdToTokenIds = Object.values(tokens)
73
153
  // ignore testnet tokens
74
154
  .filter(({
75
155
  isTestnet
76
- }) => !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.
77
171
 
78
- // ignore tokens which don't have a coingeckoId
79
- .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
+ })
80
179
 
81
180
  // get each token's coingeckoId
82
181
  .reduce((coingeckoIdToTokenIds, {
@@ -89,31 +188,55 @@ async function fetchTokenRates(tokens) {
89
188
  }, {});
90
189
 
91
190
  // create a list of coingeckoIds we want to fetch
92
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
191
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
93
192
 
94
193
  // skip network request if there is nothing for us to fetch
95
194
  if (coingeckoIds.length < 1) return {};
96
195
 
97
- // construct a coingecko request
98
- const idsSerialized = coingeckoIds.join(",");
99
- const currenciesSerialized = coingeckoCurrencies.join(",");
100
- 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
+ };
101
208
 
102
209
  // fetch the token prices from coingecko
103
210
  // the response should be in the format:
104
211
  // {
105
212
  // [coingeckoId]: {
106
213
  // [currency]: rate
214
+ // [currency_24h_change]: percent
215
+ // [currency_market_cap]: value
107
216
  // }
108
217
  // }
109
- 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));
110
229
 
111
230
  // build a TokenRatesList from the token prices result
112
231
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
113
232
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
114
- const rates = NewTokenRates();
233
+ const rates = newTokenRates();
115
234
  for (const currency of coingeckoCurrencies) {
116
- 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
+ };
117
240
  }
118
241
  return tokenIds.map(tokenId => [tokenId, rates]);
119
242
  }));
@@ -121,12 +244,15 @@ async function fetchTokenRates(tokens) {
121
244
  // return the TokenRatesList
122
245
  return ratesList;
123
246
  }
124
- function hasCoingeckoId(token) {
125
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
126
- }
127
247
 
128
- 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;
129
254
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
+ exports.TokenRatesError = TokenRatesError;
130
256
  exports.db = db;
131
257
  exports.fetchTokenRates = fetchTokenRates;
132
- 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,60 +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,
119
+ rub: null,
42
120
  krw: null,
43
- cny: null,
44
- btc: null,
45
- eth: null,
46
- dot: 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,
47
130
  });
48
131
 
49
- // the base url of the v3 coingecko api
50
- 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
+ }
51
138
 
52
139
  // every currency in this list will be fetched from coingecko
53
140
  // comment out unused currencies to save some bandwidth!
54
- const coingeckoCurrencies = ["usd",
55
- // 'aud',
56
- // 'nzd',
57
- // 'cud',
58
- // 'hkd',
59
- "eur"
60
- // 'gbp',
61
- // 'jpy',
62
- // 'krw',
63
- // 'cny',
64
- // 'btc',
65
- // 'eth',
66
- // 'dot',
67
- ];
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
+ };
68
148
 
69
149
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
70
- async function fetchTokenRates(tokens) {
150
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
71
151
  // create a map from `coingeckoId` -> `tokenId` for each token
72
152
  const coingeckoIdToTokenIds = Object.values(tokens)
73
153
  // ignore testnet tokens
74
154
  .filter(({
75
155
  isTestnet
76
- }) => !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.
77
171
 
78
- // ignore tokens which don't have a coingeckoId
79
- .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
+ })
80
179
 
81
180
  // get each token's coingeckoId
82
181
  .reduce((coingeckoIdToTokenIds, {
@@ -89,31 +188,55 @@ async function fetchTokenRates(tokens) {
89
188
  }, {});
90
189
 
91
190
  // create a list of coingeckoIds we want to fetch
92
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
191
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
93
192
 
94
193
  // skip network request if there is nothing for us to fetch
95
194
  if (coingeckoIds.length < 1) return {};
96
195
 
97
- // construct a coingecko request
98
- const idsSerialized = coingeckoIds.join(",");
99
- const currenciesSerialized = coingeckoCurrencies.join(",");
100
- 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
+ };
101
208
 
102
209
  // fetch the token prices from coingecko
103
210
  // the response should be in the format:
104
211
  // {
105
212
  // [coingeckoId]: {
106
213
  // [currency]: rate
214
+ // [currency_24h_change]: percent
215
+ // [currency_market_cap]: value
107
216
  // }
108
217
  // }
109
- 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));
110
229
 
111
230
  // build a TokenRatesList from the token prices result
112
231
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
113
232
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
114
- const rates = NewTokenRates();
233
+ const rates = newTokenRates();
115
234
  for (const currency of coingeckoCurrencies) {
116
- 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
+ };
117
240
  }
118
241
  return tokenIds.map(tokenId => [tokenId, rates]);
119
242
  }));
@@ -121,12 +244,15 @@ async function fetchTokenRates(tokens) {
121
244
  // return the TokenRatesList
122
245
  return ratesList;
123
246
  }
124
- function hasCoingeckoId(token) {
125
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
126
- }
127
247
 
128
- 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;
129
254
  exports.TalismanTokenRatesDatabase = TalismanTokenRatesDatabase;
255
+ exports.TokenRatesError = TokenRatesError;
130
256
  exports.db = db;
131
257
  exports.fetchTokenRates = fetchTokenRates;
132
- 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,60 +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,
117
+ rub: null,
34
118
  krw: null,
35
- cny: null,
36
- btc: null,
37
- eth: null,
38
- dot: 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,
39
128
  });
40
129
 
41
- // the base url of the v3 coingecko api
42
- 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
+ }
43
136
 
44
137
  // every currency in this list will be fetched from coingecko
45
138
  // comment out unused currencies to save some bandwidth!
46
- const coingeckoCurrencies = ["usd",
47
- // 'aud',
48
- // 'nzd',
49
- // 'cud',
50
- // 'hkd',
51
- "eur"
52
- // 'gbp',
53
- // 'jpy',
54
- // 'krw',
55
- // 'cny',
56
- // 'btc',
57
- // 'eth',
58
- // 'dot',
59
- ];
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
+ };
60
146
 
61
147
  // export function tokenRates(tokens: WithCoingeckoId[]): TokenRatesList {}
62
- async function fetchTokenRates(tokens) {
148
+ async function fetchTokenRates(tokens, config = DEFAULT_COINGECKO_CONFIG) {
63
149
  // create a map from `coingeckoId` -> `tokenId` for each token
64
150
  const coingeckoIdToTokenIds = Object.values(tokens)
65
151
  // ignore testnet tokens
66
152
  .filter(({
67
153
  isTestnet
68
- }) => !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.
69
169
 
70
- // ignore tokens which don't have a coingeckoId
71
- .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
+ })
72
177
 
73
178
  // get each token's coingeckoId
74
179
  .reduce((coingeckoIdToTokenIds, {
@@ -81,31 +186,55 @@ async function fetchTokenRates(tokens) {
81
186
  }, {});
82
187
 
83
188
  // create a list of coingeckoIds we want to fetch
84
- const coingeckoIds = Object.keys(coingeckoIdToTokenIds);
189
+ const coingeckoIds = Object.keys(coingeckoIdToTokenIds).sort();
85
190
 
86
191
  // skip network request if there is nothing for us to fetch
87
192
  if (coingeckoIds.length < 1) return {};
88
193
 
89
- // construct a coingecko request
90
- const idsSerialized = coingeckoIds.join(",");
91
- const currenciesSerialized = coingeckoCurrencies.join(",");
92
- 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
+ };
93
206
 
94
207
  // fetch the token prices from coingecko
95
208
  // the response should be in the format:
96
209
  // {
97
210
  // [coingeckoId]: {
98
211
  // [currency]: rate
212
+ // [currency_24h_change]: percent
213
+ // [currency_market_cap]: value
99
214
  // }
100
215
  // }
101
- 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));
102
227
 
103
228
  // build a TokenRatesList from the token prices result
104
229
  const ratesList = Object.fromEntries(coingeckoIds.flatMap(coingeckoId => {
105
230
  const tokenIds = coingeckoIdToTokenIds[coingeckoId];
106
- const rates = NewTokenRates();
231
+ const rates = newTokenRates();
107
232
  for (const currency of coingeckoCurrencies) {
108
- 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
+ };
109
238
  }
110
239
  return tokenIds.map(tokenId => [tokenId, rates]);
111
240
  }));
@@ -113,8 +242,9 @@ async function fetchTokenRates(tokens) {
113
242
  // return the TokenRatesList
114
243
  return ratesList;
115
244
  }
116
- function hasCoingeckoId(token) {
117
- return "coingeckoId" in token && typeof token.coingeckoId === "string";
118
- }
119
245
 
120
- 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.2.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.7.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.2",
28
+ "@types/jest": "^29.5.14",
29
+ "eslint": "^8.57.1",
30
+ "jest": "^29.7.0",
31
+ "ts-jest": "^29.2.5",
32
+ "typescript": "^5.6.3",
35
33
  "@talismn/tsconfig": "0.0.2",
36
- "@types/jest": "^27.5.1",
37
- "eslint": "^8.4.0",
38
- "jest": "^28.1.0",
39
- "ts-jest": "^28.0.2",
40
- "typescript": "^4.6.4"
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,158 +0,0 @@
1
- # @talismn/token-rates
2
-
3
- ## 0.2.0
4
-
5
- ### Minor Changes
6
-
7
- - b920ab98: Added GPL licence
8
-
9
- ### Patch Changes
10
-
11
- - 3c1a8b10: Dependency updates
12
- - Updated dependencies [3c1a8b10]
13
- - Updated dependencies [b920ab98]
14
- - @talismn/chaindata-provider@0.7.0
15
-
16
- ## 0.1.18
17
-
18
- ### Patch Changes
19
-
20
- - @talismn/chaindata-provider@0.6.0
21
-
22
- ## 0.1.17
23
-
24
- ### Patch Changes
25
-
26
- - Updated dependencies [1a2fdc73]
27
- - @talismn/chaindata-provider@0.5.0
28
-
29
- ## 0.1.16
30
-
31
- ### Patch Changes
32
-
33
- - f7aca48b: eslint rules
34
- - 01bf239b: fix: packages publishing with incorrect interdependency versions
35
- - Updated dependencies [f7aca48b]
36
- - Updated dependencies [48f0222e]
37
- - Updated dependencies [01bf239b]
38
- - @talismn/chaindata-provider@0.4.4
39
-
40
- ## 0.1.15
41
-
42
- ### Patch Changes
43
-
44
- - 6643a4e4: fix: tokenRates in @talismn/balances-react
45
- - Updated dependencies [79f6ccf6]
46
- - Updated dependencies [c24dc1fb]
47
- - @talismn/chaindata-provider@0.4.3
48
-
49
- ## 0.1.14
50
-
51
- ### Patch Changes
52
-
53
- - @talismn/chaindata-provider@0.4.2
54
-
55
- ## 0.1.13
56
-
57
- ### Patch Changes
58
-
59
- - 8adc7f06: feat: switched build tool to preconstruct
60
- - Updated dependencies [8adc7f06]
61
- - @talismn/chaindata-provider@0.4.1
62
-
63
- ## 0.1.12
64
-
65
- ### Patch Changes
66
-
67
- - 4aa691d: feat: new balance modules
68
- - Updated dependencies [4aa691d]
69
- - @talismn/chaindata-provider@0.2.1
70
-
71
- ## 0.1.11
72
-
73
- ### Patch Changes
74
-
75
- - @talismn/chaindata-provider@0.2.0
76
-
77
- ## 0.1.10
78
-
79
- ### Patch Changes
80
-
81
- - fix: a variety of improvements from the wallet integration
82
- - Updated dependencies
83
- - @talismn/chaindata-provider@0.1.10
84
-
85
- ## 0.1.9
86
-
87
- ### Patch Changes
88
-
89
- - Updated dependencies [8ecb8214]
90
- - @talismn/chaindata-provider@0.1.9
91
-
92
- ## 0.1.8
93
-
94
- ### Patch Changes
95
-
96
- - @talismn/chaindata-provider@0.1.8
97
-
98
- ## 0.1.7
99
-
100
- ### Patch Changes
101
-
102
- - db04d0d: fix: missing token rates and empty token rates requests
103
- - @talismn/chaindata-provider@0.1.7
104
-
105
- ## 0.1.6
106
-
107
- ### Patch Changes
108
-
109
- - ca50757: feat: implemented token fiat rates in @talismn/balances
110
- - Updated dependencies [ca50757]
111
- - @talismn/chaindata-provider@0.1.6
112
-
113
- ## 0.1.5
114
-
115
- ### Patch Changes
116
-
117
- - Updated dependencies [d66c5bc]
118
- - @talismn/chaindata-provider@0.1.5
119
-
120
- ## 0.1.4
121
-
122
- ### Patch Changes
123
-
124
- - @talismn/chaindata-provider@0.1.4
125
-
126
- ## 0.1.3
127
-
128
- ### Patch Changes
129
-
130
- - Updated dependencies [d5f69f7]
131
- - @talismn/chaindata-provider@0.1.3
132
-
133
- ## 0.1.2
134
-
135
- ### Patch Changes
136
-
137
- - 5af305c: switched build output from esm to commonjs for ecosystem compatibility
138
- - Updated dependencies [5af305c]
139
- - @talismn/chaindata-provider@0.1.2
140
-
141
- ## 0.1.1
142
-
143
- ### Patch Changes
144
-
145
- - Fixed publish config
146
- - Updated dependencies
147
- - @talismn/chaindata-provider@0.1.1
148
-
149
- ## 0.1.0
150
-
151
- ### Minor Changes
152
-
153
- - 43c1a3a: Initial release
154
-
155
- ### Patch Changes
156
-
157
- - Updated dependencies [43c1a3a]
158
- - @talismn/chaindata-provider@0.1.0