@merkl/api 0.18.4 → 0.18.6

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.
@@ -3,6 +3,7 @@ import { getTokensListWithCache } from "@/libs/getTokensList";
3
3
  import { getOnlyUserBalance } from "@/libs/tokens/balances";
4
4
  import { log } from "@/utils/logger";
5
5
  import { apiDbClient } from "@/utils/prisma";
6
+ import { throwOnInvalidRequiredAddress, throwOnUnsupportedChainId } from "@/utils/throw";
6
7
  import { Prisma } from "@db/api";
7
8
  import { ChainInteractionService, DistributionCreatorService, NETWORK_LABELS, bigIntToNumber, } from "@sdk";
8
9
  import { getAddress, parseUnits } from "viem";
@@ -71,7 +72,6 @@ export class TokenService {
71
72
  }
72
73
  static async fetchOnChain(token) {
73
74
  const onchainData = await TokenRepository.getTokenInfo(token);
74
- //TODO: find a way to get the icon
75
75
  return {
76
76
  chainId: token.chainId,
77
77
  address: token.address,
@@ -79,9 +79,19 @@ export class TokenService {
79
79
  ...Object.assign({ name: "unknown", decimals: 18, symbol: "UNKNOWN", verified: false, isTest: false, isNative: false }, onchainData),
80
80
  };
81
81
  }
82
+ static async tryToFillWithCoingeckoIcons() {
83
+ // 1 - Find coingecko price sources
84
+ let priceSources = await PriceService.findManyPriceSources({ method: "COINGECKO" });
85
+ // 2 - Find tokens with missing logos
86
+ const missingIcons = await TokenService.findMany({ missingIcons: true });
87
+ // 3 - Do the intersection of both
88
+ priceSources = priceSources.filter(p => missingIcons.find(t => t.symbol === p.symbol || t.address === p.symbol));
89
+ log.info(`found ${priceSources.length} tokens with missing icons on coingecko.`);
90
+ // 4 - Loop through each and try to get the icon
91
+ }
82
92
  static async fetchManyOnChain(chainId, addresses) {
83
93
  const tokens = {};
84
- //Batch onchain calls together when multiples
94
+ // Batch onchain calls together when multiples
85
95
  for (const address of addresses) {
86
96
  tokens[address] = await TokenService.fetchOnChain({ chainId, address });
87
97
  }
@@ -127,77 +137,6 @@ export class TokenService {
127
137
  await TokenService.updateAddressPrices(address, price);
128
138
  }
129
139
  }
130
- /**
131
- * Get all tokens from https://github.com/AngleProtocol/angle-token-list and override icons from it
132
- * TODO: use the bucket
133
- */
134
- static async fillTokenAndIconsFromTokenList() {
135
- const tokenList = await getTokensListWithCache();
136
- for (const chain of Object.keys(tokenList)) {
137
- for (const [symbol, token] of Object.entries(tokenList[chain])) {
138
- if (!(await TokenRepository.findUnique(TokenService.hashId({ chainId: Number.parseInt(chain), address: token.address })))) {
139
- try {
140
- const res = await TokenRepository.create({
141
- id: TokenService.hashId({ chainId: Number.parseInt(chain), address: token.address }),
142
- chainId: Number.parseInt(chain),
143
- address: token.address,
144
- name: token.name,
145
- symbol: token.symbol,
146
- verified: true,
147
- decimals: token.decimals,
148
- icon: token.logoURI,
149
- isTest: false,
150
- isNative: false,
151
- });
152
- log.local(`Token created: ${res?.symbol} on ${NETWORK_LABELS[Number.parseInt(chain)]}`);
153
- }
154
- catch (e) {
155
- console.error(e);
156
- }
157
- }
158
- try {
159
- await apiDbClient.token.update({
160
- data: {
161
- chainId: Number.parseInt(chain),
162
- address: token.address,
163
- name: token.name,
164
- symbol: token.symbol,
165
- verified: true,
166
- decimals: token.decimals,
167
- icon: token.logoURI,
168
- },
169
- where: {
170
- chainId_address: {
171
- chainId: Number.parseInt(chain),
172
- address: token.address,
173
- },
174
- },
175
- });
176
- }
177
- catch (e) {
178
- console.error(e);
179
- }
180
- try {
181
- const tokensWithSameSymbol = await apiDbClient.token.findMany({
182
- select: { chainId: true, address: true },
183
- where: { symbol: { equals: symbol, mode: "insensitive" } },
184
- });
185
- for (const dbToken of tokensWithSameSymbol) {
186
- await apiDbClient.token.update({
187
- data: { icon: token.logoURI },
188
- where: {
189
- chainId_address: {
190
- chainId: dbToken.chainId,
191
- address: dbToken.address,
192
- },
193
- },
194
- });
195
- }
196
- }
197
- catch (_err) { }
198
- }
199
- }
200
- }
201
140
  /**
202
141
  * Create token on database
203
142
  * @param chainId
@@ -320,24 +259,21 @@ export class TokenService {
320
259
  }, chainId);
321
260
  }
322
261
  static async update(id, data) {
323
- // let iconUri = data.icon;
324
- // if (data.iconFile) {
325
- // iconUri = await BucketService.upload("merkl-assets", `/tokens/${id}`, data.iconFile.stream(), true);
326
- // }
327
262
  return await TokenRepository.update(id, data);
328
263
  }
329
264
  static async notionWebhook(body) {
330
265
  const env = process.env.ENV === "prod" ? "production" : process.env.ENV;
331
266
  const bucket = new BucketService(`merkl-${env}-tokens`, `angle-${env}-1`);
332
267
  const properties = body.data.properties;
333
- const icon = properties["Icon"].files?.[0]?.file.url;
268
+ const icon = properties.Icon.files?.[0]?.file.url;
334
269
  const iconFile = await fetch(icon);
335
270
  const mimeType = iconFile.headers.get("content-type");
336
271
  const extension = mimeType.split("/")[1];
337
- const address = properties["Address"].rich_text[0].plain_text;
272
+ const address = throwOnInvalidRequiredAddress(properties.Address.rich_text[0].plain_text);
338
273
  const chainId = properties["Chain ID"].number;
339
- const symbol = properties["Symbol"].rich_text[0]?.plain_text;
340
- const isVerified = properties["Verified"].checkbox;
274
+ throwOnUnsupportedChainId(chainId);
275
+ const displaySymbol = properties.Symbol.rich_text[0]?.plain_text;
276
+ const isVerified = properties.Verified.checkbox;
341
277
  const coingeckoApiId = properties["CoinGecko API ID"].rich_text[0].plain_text;
342
278
  const byteArray = await iconFile.bytes();
343
279
  const [token] = await TokenService.getManyOrCreate([
@@ -347,7 +283,7 @@ export class TokenService {
347
283
  },
348
284
  ]);
349
285
  if (!token || !token.name)
350
- throw new HttpError(`Failed to fetch on-chain data for token ${symbol} (${address} on chainId ${chainId}).`);
286
+ throw new HttpError(`Failed to fetch on-chain data for token ${token?.symbol} (${address} on chainId ${chainId}).`);
351
287
  try {
352
288
  await bucket.pushRaw(`${chainId}/${address}.${extension}`, byteArray, {
353
289
  type: mimeType,
@@ -361,7 +297,11 @@ export class TokenService {
361
297
  }
362
298
  if (coingeckoApiId) {
363
299
  try {
364
- await PriceService.createPriceSource({ method: "COINGECKO", symbol, args: { ticker: coingeckoApiId } });
300
+ await PriceService.createPriceSource({
301
+ method: "COINGECKO",
302
+ symbol: token.symbol,
303
+ args: { ticker: coingeckoApiId },
304
+ });
365
305
  }
366
306
  catch (err) {
367
307
  console.error("Failed to create price source.");
@@ -370,9 +310,81 @@ export class TokenService {
370
310
  }
371
311
  return await TokenService.update(token.id, {
372
312
  icon: token.icon,
373
- displaySymbol: symbol && symbol !== "" ? symbol : token.symbol,
313
+ displaySymbol: displaySymbol && displaySymbol !== "" ? displaySymbol : token.symbol,
374
314
  name: token.name,
375
315
  verified: isVerified,
376
316
  });
377
317
  }
318
+ /**
319
+ * Get all tokens from https://github.com/AngleProtocol/angle-token-list and override icons from it
320
+ * TODO: use the bucket
321
+ * @deprecated Should be useless now that the token list is not used anymore
322
+ */
323
+ static async fillTokenAndIconsFromTokenList() {
324
+ const tokenList = await getTokensListWithCache();
325
+ for (const chain of Object.keys(tokenList)) {
326
+ for (const [symbol, token] of Object.entries(tokenList[chain])) {
327
+ if (!(await TokenRepository.findUnique(TokenService.hashId({ chainId: Number.parseInt(chain), address: token.address })))) {
328
+ try {
329
+ const res = await TokenRepository.create({
330
+ id: TokenService.hashId({ chainId: Number.parseInt(chain), address: token.address }),
331
+ chainId: Number.parseInt(chain),
332
+ address: token.address,
333
+ name: token.name,
334
+ symbol: token.symbol,
335
+ verified: true,
336
+ decimals: token.decimals,
337
+ icon: token.logoURI,
338
+ isTest: false,
339
+ isNative: false,
340
+ });
341
+ log.local(`Token created: ${res?.symbol} on ${NETWORK_LABELS[Number.parseInt(chain)]}`);
342
+ }
343
+ catch (e) {
344
+ console.error(e);
345
+ }
346
+ }
347
+ try {
348
+ await apiDbClient.token.update({
349
+ data: {
350
+ chainId: Number.parseInt(chain),
351
+ address: token.address,
352
+ name: token.name,
353
+ symbol: token.symbol,
354
+ verified: true,
355
+ decimals: token.decimals,
356
+ icon: token.logoURI,
357
+ },
358
+ where: {
359
+ chainId_address: {
360
+ chainId: Number.parseInt(chain),
361
+ address: token.address,
362
+ },
363
+ },
364
+ });
365
+ }
366
+ catch (e) {
367
+ console.error(e);
368
+ }
369
+ try {
370
+ const tokensWithSameSymbol = await apiDbClient.token.findMany({
371
+ select: { chainId: true, address: true },
372
+ where: { symbol: { equals: symbol, mode: "insensitive" } },
373
+ });
374
+ for (const dbToken of tokensWithSameSymbol) {
375
+ await apiDbClient.token.update({
376
+ data: { icon: token.logoURI },
377
+ where: {
378
+ chainId_address: {
379
+ chainId: dbToken.chainId,
380
+ address: dbToken.address,
381
+ },
382
+ },
383
+ });
384
+ }
385
+ }
386
+ catch (_err) { }
387
+ }
388
+ }
389
+ }
378
390
  }
@@ -19,7 +19,7 @@ export default (app) => app.get("/app", async () => {
19
19
  if (!json.tokens[token.chainId]) {
20
20
  json.tokens[token.chainId] = {};
21
21
  }
22
- if (!json.tokens[token.chainId][token.address]) {
22
+ if (!json.tokens[token.chainId][token.address] && token.icon?.length > 0) {
23
23
  json.tokens[token.chainId][token.address] = {
24
24
  address: token.address,
25
25
  chainId: token.chainId,
@@ -26,7 +26,7 @@ export default class PriceService {
26
26
  });
27
27
  };
28
28
  async fetchPrices() {
29
- const tokenPriceSources = await PriceSourceService.getManyPriceSources();
29
+ const tokenPriceSources = await PriceSourceService.findManyPriceSources();
30
30
  /**
31
31
  * @description Factory pricer's call to get prices from different sources
32
32
  */