Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getPushNotificationsRepository,
getPushSubscriptionsRepository,
getSimulationRepository,
getTokenCacheRepository,
getTokenHolderRepository,
getUsdRepository,
} from '@cowprotocol/services';
Expand All @@ -14,13 +15,15 @@ import {
PushNotificationsRepository,
PushSubscriptionsRepository,
SimulationRepository,
TokenCacheRepository,
TokenHolderRepository,
UsdRepository,
cacheRepositorySymbol,
erc20RepositorySymbol,
pushNotificationsRepositorySymbol,
pushSubscriptionsRepositorySymbol,
tenderlyRepositorySymbol,
tokenCacheRepositorySymbol,
tokenHolderRepositorySymbol,
usdRepositorySymbol,
} from '@cowprotocol/repositories';
Expand Down Expand Up @@ -52,6 +55,7 @@ function getApiContainer(): Container {
const cacheRepository = getCacheRepository();
const erc20Repository = getErc20Repository(cacheRepository);
const simulationRepository = getSimulationRepository();
const tokenCacheRepository = getTokenCacheRepository();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard token-cache DI binding; don’t crash when Redis is absent

getTokenCacheRepository() throws without Redis. Wrap creation, bind only if available, and keep startup consistent with main.ts’ graceful path.

-  const tokenCacheRepository = getTokenCacheRepository();
+  let tokenCacheRepository: TokenCacheRepository | undefined;
+  try {
+    tokenCacheRepository = getTokenCacheRepository();
+  } catch (err) {
+    logger.warn('Token cache repository not initialized; proceeding without Redis', err);
+  }
...
-  apiContainer
-    .bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
-    .toConstantValue(tokenCacheRepository);
+  if (tokenCacheRepository) {
+    apiContainer
+      .bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
+      .toConstantValue(tokenCacheRepository);
+  }

Longer-term, consider sourcing the single instance from DI and calling setTokenCacheRepository() with that instance to avoid duplication and ensure consistent lifecycle.

Also applies to: 92-95

🤖 Prompt for AI Agents
In apps/api/src/app/inversify.config.ts around line 58 (and similarly lines
92-95), getTokenCacheRepository() can throw when Redis is unavailable; wrap the
repository creation in a try/catch and only perform the DI bind when creation
succeeds, logging or no-oping on failure to preserve the existing graceful
startup path used in main.ts; as a follow-up consider obtaining a single
token-cache instance via DI and calling setTokenCacheRepository(instance) so the
instance lifecycle is consistent and not duplicated.

const tokenHolderRepository = getTokenHolderRepository(cacheRepository);
const usdRepository = getUsdRepository(cacheRepository, erc20Repository);
const pushNotificationsRepository = getPushNotificationsRepository();
Expand Down Expand Up @@ -85,6 +89,10 @@ function getApiContainer(): Container {
.bind<TokenHolderRepository>(tokenHolderRepositorySymbol)
.toConstantValue(tokenHolderRepository);

apiContainer
.bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
.toConstantValue(tokenCacheRepository);

// Services
apiContainer
.bind<SlippageService>(slippageServiceSymbol)
Expand Down
47 changes: 47 additions & 0 deletions apps/api/src/app/routes/__chainId/tokens/initTokenList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { initTokenList } from '@cowprotocol/repositories';
import { FastifyPluginAsync } from 'fastify';
import {
errorSchema,
ErrorSchema,
paramsSchema,
RouteSchema,
successSchema,
SuccessSchema,
} from './schemas';

const root: FastifyPluginAsync = async (fastify): Promise<void> => {
// example: http://localhost:3001/1/tokens/initTokenList
fastify.get<{
Params: RouteSchema;
Reply: SuccessSchema | ErrorSchema;
}>(
'/',
{
schema: {
params: paramsSchema,
response: {
'2XX': successSchema,
'404': errorSchema,
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Describe 500 (and 400) responses in the route schema

Handler returns 500, but schema only defines 2XX and 404. Add 500; consider 400 for “unsupported chain” errors.

Apply:

       schema: {
         params: paramsSchema,
         response: {
-          '2XX': successSchema,
-          '404': errorSchema,
+          200: successSchema,
+          400: errorSchema,
+          500: errorSchema,
         },
       },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/api/src/app/routes/__chainId/tokens/initTokenList/index.ts around lines
20 to 26, the route schema only declares '2XX' and '404' responses while the
handler can return 500 (and may return 400 for unsupported chain), so update the
schema.response to add a '500' entry pointing to an error schema (and add a
'400' entry if you return bad-request for unsupported chain) so those error
shapes are documented; ensure the added entries reference the existing
errorSchema (or a specific badRequestSchema if you have one) and update any
related types/tests as needed.

},
async function (request, reply) {
const { chainId } = request.params;

try {
await initTokenList(chainId);

fastify.log.info(`Token list initalized for chain ${chainId}`);

reply.send(true);
} catch (error) {
fastify.log.error('Error searching tokens:', error);
reply.code(500).send({
message: 'Internal server error while searching tokens',
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix log/message text and map known errors to proper status codes

Aligns copy with route intent and sends 400 for unsupported chains.

Apply:

-      try {
-        await initTokenList(chainId);
-
-        fastify.log.info(`Token list initalized for chain ${chainId}`);
-
-        reply.send(true);
-      } catch (error) {
-        fastify.log.error('Error searching tokens:', error);
-        reply.code(500).send({
-          message: 'Internal server error while searching tokens',
-        });
-      }
+      try {
+        await initTokenList(chainId);
+        fastify.log.info(`Token list initialized for chain ${chainId}`);
+        reply.send(true);
+      } catch (error) {
+        const msg = (error as Error)?.message ?? 'Unknown error';
+        fastify.log.error('Error initializing token list:', error);
+        if (/not supported by CoinGecko/i.test(msg)) {
+          return reply.code(400).send({ message: msg });
+        }
+        reply.code(500).send({
+          message: 'Internal server error while initializing token list',
+        });
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await initTokenList(chainId);
fastify.log.info(`Token list initalized for chain ${chainId}`);
reply.send(true);
} catch (error) {
fastify.log.error('Error searching tokens:', error);
reply.code(500).send({
message: 'Internal server error while searching tokens',
});
try {
await initTokenList(chainId);
fastify.log.info(`Token list initialized for chain ${chainId}`);
reply.send(true);
} catch (error) {
const msg = (error as Error)?.message ?? 'Unknown error';
fastify.log.error('Error initializing token list:', error);
if (/not supported by CoinGecko/i.test(msg)) {
return reply.code(400).send({ message: msg });
}
reply.code(500).send({
message: 'Internal server error while initializing token list',
});
}
🤖 Prompt for AI Agents
In apps/api/src/app/routes/__chainId/tokens/initTokenList/index.ts around lines
31 to 41, the catch block uses incorrect log/message text and always returns
500; update the log to reflect initialization (e.g., "Error initializing token
list for chain:") and change the response mapping so known unsupported-chain
errors return 400. Specifically, detect the unsupported-chain error (e.g.,
error.name === 'UnsupportedChainError' or instanceof UnsupportedChainError) and
reply.code(400).send({ message: 'Unsupported chain' }); otherwise log the error
and reply.code(500).send({ message: 'Internal server error while initializing
token list' }). Ensure the log includes the error object/details.

}
}
);
};

export default root;
32 changes: 32 additions & 0 deletions apps/api/src/app/routes/__chainId/tokens/initTokenList/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { SupportedChainIdSchema } from '../../../../schemas';

export const paramsSchema = {
type: 'object',
required: ['chainId'],
additionalProperties: false,
properties: {
chainId: SupportedChainIdSchema,
},
} as const satisfies JSONSchema;

export const successSchema = {
type: 'boolean',
} as const satisfies JSONSchema;

export const errorSchema = {
type: 'object',
required: ['message'],
additionalProperties: false,
properties: {
message: {
title: 'Message',
description: 'Message describing the error.',
type: 'string',
},
},
} as const satisfies JSONSchema;

export type RouteSchema = FromSchema<typeof paramsSchema>;
export type SuccessSchema = FromSchema<typeof successSchema>;
export type ErrorSchema = FromSchema<typeof errorSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { getTokenListBySearchParam } from '@cowprotocol/repositories';
import { FastifyPluginAsync } from 'fastify';
import {
errorSchema,
ErrorSchema,
paramsSchema,
RouteSchema,
successSchema,
SuccessSchema,
} from './schemas';

const root: FastifyPluginAsync = async (fastify): Promise<void> => {
// example: http://localhost:3010/1/tokens/search/USDC
fastify.get<{
Params: RouteSchema;
Reply: SuccessSchema | ErrorSchema;
}>(
'/',
{
schema: {
params: paramsSchema,
response: {
'2XX': successSchema,
'404': errorSchema,
},
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
async function (request, reply) {
const { chainId, searchParam } = request.params;

try {
const tokens = await getTokenListBySearchParam(chainId, searchParam);

fastify.log.info(
`Token search for "${searchParam}" on chain ${chainId}: ${tokens.length} tokens found`
);

reply.send(tokens);
} catch (error) {
fastify.log.error('Error searching tokens:', error);
reply.code(500).send({
message: 'Internal server error while searching tokens',
});
}
}
);
};

export default root;
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { SupportedChainIdSchema } from '../../../../../schemas';
import { AllChainIds } from '@cowprotocol/shared';

export const paramsSchema = {
type: 'object',
required: ['chainId', 'searchParam'],
additionalProperties: false,
properties: {
chainId: SupportedChainIdSchema,
searchParam: {
title: 'Search Parameter',
description: 'Token search parameter (name, symbol, or address)',
type: 'string',
minLength: 3,
maxLength: 100,
},
},
} as const satisfies JSONSchema;

export const successSchema = {
type: 'array',
items: {
type: 'object',
required: ['chainId', 'address', 'name', 'symbol', 'decimals', 'logoURI'],
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say the logoURI is optional

Suggested change
required: ['chainId', 'address', 'name', 'symbol', 'decimals', 'logoURI'],
required: ['chainId', 'address', 'name', 'symbol', 'decimals'],

additionalProperties: false,
properties: {
chainId: {
title: 'Chain ID',
description: 'Blockchain network identifier.',
type: 'integer',
enum: AllChainIds,
},
address: {
title: 'Token Address',
description: 'Contract address of the token.',
type: 'string',
pattern: '^0x[a-fA-F0-9]{40}$',
},
name: {
title: 'Name',
description: 'Full name of the token.',
type: 'string',
},
symbol: {
title: 'Symbol',
description: 'Token symbol/ticker.',
type: 'string',
},
decimals: {
title: 'Decimals',
description: 'Number of decimal places for the token.',
type: 'integer',
minimum: 0,
maximum: 18,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standard is 18, but it's actually not the max AFAICT.

https://ethereum.stackexchange.com/questions/118896/can-an-erc-20-have-more-than-18-decimals.

I'd remove it:

Suggested change
maximum: 18,

},
logoURI: {
title: 'Logo URI',
description: 'URI to the token logo.',
type: 'string',
},
},
},
} as const satisfies JSONSchema;

export const errorSchema = {
type: 'object',
required: ['message'],
additionalProperties: false,
properties: {
message: {
title: 'Message',
description: 'Message describing the error.',
type: 'string',
},
},
} as const satisfies JSONSchema;

export type RouteSchema = FromSchema<typeof paramsSchema>;
export type SuccessSchema = FromSchema<typeof successSchema>;
export type ErrorSchema = FromSchema<typeof errorSchema>;
11 changes: 11 additions & 0 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Fastify from 'fastify';
import { app } from './app/app';
import { logger } from '@cowprotocol/shared';
import { getTokenCacheRepository } from '@cowprotocol/services';
import { setTokenCacheRepository } from '@cowprotocol/repositories';

const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ? Number(process.env.PORT) : 3001;
Expand All @@ -10,6 +12,15 @@ export const server = Fastify({
logger,
});

// Initialize token cache repository
try {
const tokenCacheRepository = getTokenCacheRepository();
setTokenCacheRepository(tokenCacheRepository);
logger.info('Token cache repository initialized with Redis');
} catch (error) {
logger.warn('Token cache repository not initialized:', error);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Startup handles missing Redis gracefully, but DI still hard-fails — align both paths

Main catches getTokenCacheRepository() failures and proceeds, but inversify.config.ts unconditionally constructs/binds the same repo and will throw if Redis isn’t configured. Result: app still crashes during container build even though this block tries to degrade gracefully.

Apply this change in apps/api/src/app/inversify.config.ts to make the binding optional and consistent with this file’s behavior:

-  const tokenCacheRepository = getTokenCacheRepository();
+  let tokenCacheRepository: TokenCacheRepository | undefined;
+  try {
+    tokenCacheRepository = getTokenCacheRepository();
+  } catch (err) {
+    logger.warn('Token cache repository not initialized; proceeding without Redis', err);
+  }
...
-  apiContainer
-    .bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
-    .toConstantValue(tokenCacheRepository);
+  if (tokenCacheRepository) {
+    apiContainer
+      .bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
+      .toConstantValue(tokenCacheRepository);
+  }

Additionally consider unifying on a single source of truth (either DI or the module-level setter) to avoid double instantiation. I can draft that follow-up if you want.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Initialize token cache repository
try {
const tokenCacheRepository = getTokenCacheRepository();
setTokenCacheRepository(tokenCacheRepository);
logger.info('Token cache repository initialized with Redis');
} catch (error) {
logger.warn('Token cache repository not initialized:', error);
}
// apps/api/src/app/inversify.config.ts
// Make the TokenCacheRepository binding optional, mirroring the behavior in main.ts
let tokenCacheRepository: TokenCacheRepository | undefined;
try {
tokenCacheRepository = getTokenCacheRepository();
} catch (err) {
logger.warn('Token cache repository not initialized; proceeding without Redis', err);
}
if (tokenCacheRepository) {
apiContainer
.bind<TokenCacheRepository>(tokenCacheRepositorySymbol)
.toConstantValue(tokenCacheRepository);
}
🤖 Prompt for AI Agents
In apps/api/src/app/inversify.config.ts (around lines 1-200), the container
unconditionally constructs and binds the Redis-backed token cache repository
which will throw if Redis isn’t configured; make the binding optional and
consistent with main.ts by wrapping the creation in a try/catch and only binding
the concrete Redis repository when construction succeeds, otherwise bind a safe
fallback (e.g., a no-op in-memory/null implementation or leave the token cache
binding absent and bind a nullable/optional token cache token). Also avoid
double-instantiation by resolving the module-level setter: if
getTokenCacheRepository() already created an instance, have the DI binding use
that instance (inject the existing instance or export a factory that returns the
module-level repository) so you don’t create two repositories.


Comment on lines +15 to +17
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Unconditional Redis dependency here can crash API startup; remove wiring from main or guard it.
If REDIS_* isn’t set, getTokenCacheRepository() throws and the process exits before Fastify starts. Prefer sourcing the instance via DI only (recommended), or guard creation.

Recommended (remove from main; let inversify.config.ts own lifecycle):

-import { getTokenCacheRepository } from '@cowprotocol/services';
-import { setTokenCacheRepository } from '@cowprotocol/repositories';
...
-const tokenCacheRepository = getTokenCacheRepository();
-setTokenCacheRepository(tokenCacheRepository);

If you must keep it in main, at least guard:

+import { getTokenCacheRepository } from '@cowprotocol/services';
+import { setTokenCacheRepository } from '@cowprotocol/repositories';
...
-const tokenCacheRepository = getTokenCacheRepository();
-setTokenCacheRepository(tokenCacheRepository);
+try {
+  const tokenCacheRepository = getTokenCacheRepository();
+  setTokenCacheRepository(tokenCacheRepository);
+  server.log.info('Token cache repository initialized');
+} catch (err) {
+  server.log.warn({ err }, 'Token cache repository not initialized; proceeding without Redis');
+}

Also applies to: 4-5

🤖 Prompt for AI Agents
In apps/api/src/main.ts around lines 15 to 17, the call to
getTokenCacheRepository() unconditionally requires Redis and can throw if
REDIS_* env vars are missing; remove this wiring from main (let
inversify.config.ts/inversion of control own the lifecycle and inject the
repository where needed) or, if you must keep it here, guard creation by
checking the required REDIS_* env vars and only call
setTokenCacheRepository(...) when they are present (or wrap
getTokenCacheRepository() in a try/catch and skip wiring on failure), ensuring
Fastify startup is not blocked by a missing Redis configuration.

// Register your application as a normal plugin.
server.register(app);

Expand Down
3 changes: 3 additions & 0 deletions apps/notification-producer/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export default {
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
transformIgnorePatterns: [
'node_modules/(?!(node-fetch|data-uri-to-buffer|fetch-blob|formdata-polyfill|@cowprotocol|@uniswap)/)',
],
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/notification-producer',
setupFilesAfterEnv: ['../../jest.setup.ts'],
Expand Down
3 changes: 3 additions & 0 deletions libs/repositories/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export default {
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
transformIgnorePatterns: [
'node_modules/(?!(node-fetch|data-uri-to-buffer|fetch-blob|formdata-polyfill|@cowprotocol|@uniswap)/)',
],
Comment on lines +9 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Centralize the ESM allowlist and verify ts-jest can transpile JS.

  • Optional DRY: move this identical transformIgnorePatterns to jest.preset.js so all projects stay in sync.
  • Ensure ts-jest can actually transform the whitelisted JS from node_modules; confirm allowJs: true in each tsconfig.spec.json. If any deps ship .mjs, you may need an additional transformer (e.g., babel-jest) or ESM settings.

Apply if centralizing in jest.preset.js:

-  transformIgnorePatterns: [
-    'node_modules/(?!(node-fetch|data-uri-to-buffer|fetch-blob|formdata-polyfill|@cowprotocol|@uniswap)/)',
-  ],

Add to jest.preset.js (outside this file):

// jest.preset.js
module.exports = {
  // ...
  transformIgnorePatterns: [
    'node_modules/(?!(node-fetch|data-uri-to-buffer|fetch-blob|formdata-polyfill|@cowprotocol|@uniswap)/)',
  ],
};

Verify config consistency and allowJs:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check allowJs in tsconfig.spec.json =="
fd -a tsconfig.spec.json | while read -r f; do
  echo "-> $f"
  rg -n '"allowJs"\s*:\s*true' "$f" || echo "allowJs not set to true"
done

echo
echo "== transformIgnorePatterns occurrences (should be identical across projects) =="
rg -n --glob "**/jest.config.ts" -C2 "transformIgnorePatterns|node_modules/\(\?\!\("

Length of output: 1511


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== transformIgnorePatterns occurrences in jest.config.ts across repo =="
rg -n --glob "**/jest.config.ts" "transformIgnorePatterns" -C2 || echo "No matches found"

echo
echo "== transformIgnorePatterns occurrences in jest.preset.js =="
rg -n --glob "**/jest.preset.js" "transformIgnorePatterns" -C2 || echo "None in jest.preset.js"

Length of output: 1395


Enable JS transpilation and centralize transformIgnorePatterns

  • Add "allowJs": true to every tsconfig.spec.json so ts-jest can transpile the whitelisted JS (none currently set).
  • DRY: move the identical transformIgnorePatterns from each jest.config.ts (e.g. libs/services/jest.config.ts:9-11, libs/repositories/jest.config.ts:9-11, apps/notification-producer/jest.config.ts:9-11) into jest.preset.js and remove the duplicates.
  • If any dependencies ship .mjs, add a JS transformer (e.g. babel-jest) or configure ESM settings in Jest.
🤖 Prompt for AI Agents
In libs/repositories/jest.config.ts around lines 9 to 11, the
transformIgnorePatterns list is duplicated across multiple jest.config.ts files
and JS files aren't allowed for ts-jest transpilation; update every
tsconfig.spec.json in the repo to include "allowJs": true so ts-jest can
transpile whitelisted JS, remove the redundant transformIgnorePatterns from this
file and the other duplicate jest.config.ts files, and instead centralize that
pattern in jest.preset.js; additionally, if any dependency ships .mjs files, add
a JS transformer (e.g. babel-jest) to the preset or configure Jest's ESM
handling so .mjs modules are transformed correctly.

moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/repositories',
setupFilesAfterEnv: ['../../jest.setup.ts'],
Expand Down
41 changes: 41 additions & 0 deletions libs/repositories/src/datasources/tokenSearch/getTokensByChain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk';
import { TokenFromAPI } from './types';

async function fetchTokensFromCoinGecko(
chainName: string,
chainId: SupportedChainId
): Promise<TokenFromAPI[]> {
const tokenSource = `https://tokens.coingecko.com/${chainName}/all.json`;

console.log(`Fetching tokens for ${chainName}`);

const response = await fetch(tokenSource);

Comment on lines +13 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add timeout to external fetch to prevent hanging requests

External calls must have timeouts. Abort the fetch after a sane limit.

Apply:

-  const response = await fetch(tokenSource);
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+  const response = await fetch(tokenSource, {
+    signal: controller.signal,
+    headers: { accept: 'application/json' },
+  });
+  clearTimeout(timeout);

Add at file top (outside the function):

const FETCH_TIMEOUT_MS = 15_000;
🤖 Prompt for AI Agents
In libs/repositories/src/datasources/tokenSearch/getTokensByChain.ts around
lines 12 to 13, the external fetch call has no timeout and can hang; add a
file-level constant FETCH_TIMEOUT_MS = 15_000 (outside the function) and wrap
the fetch with an AbortController: create controller, start a timer that calls
controller.abort() after FETCH_TIMEOUT_MS, pass controller.signal to fetch,
await fetch, clear the timer on success, and handle AbortError appropriately so
the request is cancelled and resources cleaned up.

if (!response.ok) {
throw new Error(
`Failed to fetch tokens from ${tokenSource}: ${response.status} ${response.statusText}`
);
}

const data = await response.json();

if (!data.tokens || !Array.isArray(data.tokens)) {
throw new Error(
`Invalid token list format from ${tokenSource}: missing or invalid tokens array`
);
}
Comment on lines +21 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden JSON parsing and shape validation

Catch parse errors explicitly and validate presence of tokens.

Apply:

-  const data = await response.json();
-
-  if (!data.tokens || !Array.isArray(data.tokens)) {
+  let data: unknown;
+  try {
+    data = await response.json();
+  } catch (e) {
+    throw new Error(
+      `Failed to parse token list JSON from ${tokenSource}: ${(e as Error).message}`
+    );
+  }
+  if (!data || typeof data !== 'object' || !Array.isArray((data as any).tokens)) {
     throw new Error(
       `Invalid token list format from ${tokenSource}: missing or invalid tokens array`
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const data = await response.json();
if (!data.tokens || !Array.isArray(data.tokens)) {
throw new Error(
`Invalid token list format from ${tokenSource}: missing or invalid tokens array`
);
}
let data: unknown;
try {
data = await response.json();
} catch (e) {
throw new Error(
`Failed to parse token list JSON from ${tokenSource}: ${(e as Error).message}`
);
}
if (!data || typeof data !== 'object' || !Array.isArray((data as any).tokens)) {
throw new Error(
`Invalid token list format from ${tokenSource}: missing or invalid tokens array`
);
}
🤖 Prompt for AI Agents
In libs/repositories/src/datasources/tokenSearch/getTokensByChain.ts around
lines 20 to 26, the call to response.json() and subsequent access to data.tokens
is not hardened; wrap the JSON parsing in a try/catch to surface parse errors
(throw a descriptive Error including tokenSource and the original parse error),
ensure the parsed value is an object before accessing properties, and then
validate that data.tokens exists and is an Array (throw a clear, contextual
Error if not). Ensure you preserve original error context when rethrowing so
callers can distinguish parse failures from shape validation failures.


console.log(`Fetched ${data.tokens.length} tokens for ${chainName}`);

return data.tokens.map((token: TokenFromAPI) => ({
...token,
chainId,
}));
Comment on lines +31 to +34
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Normalize mapped fields and provide safe defaults

Do not trust upstream types blindly; map and coerce to our contract.

Apply:

-  return data.tokens.map((token: TokenFromAPI) => ({
-    ...token,
-    chainId,
-  }));
+  const { tokens } = data as { tokens: any[] };
+  return tokens.map((t: any): TokenFromAPI => ({
+    chainId,
+    address: String(t?.address ?? ''),
+    name: String(t?.name ?? ''),
+    symbol: String(t?.symbol ?? ''),
+    decimals: Number.isFinite(t?.decimals) ? Number(t.decimals) : 18,
+    logoURI: typeof t?.logoURI === 'string' ? t.logoURI : '',
+  }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return data.tokens.map((token: TokenFromAPI) => ({
...token,
chainId,
}));
// Normalize mapped fields and provide safe defaults
const { tokens } = data as { tokens: any[] };
return tokens.map((t: any): TokenFromAPI => ({
chainId,
address: String(t?.address ?? ''),
name: String(t?.name ?? ''),
symbol: String(t?.symbol ?? ''),
decimals: Number.isFinite(t?.decimals) ? Number(t.decimals) : 18,
logoURI: typeof t?.logoURI === 'string' ? t.logoURI : '',
}));
🤖 Prompt for AI Agents
In libs/repositories/src/datasources/tokenSearch/getTokensByChain.ts around
lines 30 to 33, the mapper blindly spreads upstream TokenFromAPI into our Token
contract; instead explicitly map each expected field (e.g., id, address, symbol,
name, decimals, logoURI, chainId) coercing types and applying safe defaults —
parse/Number.decimals fallback to 0, address normalized to lowercase and
empty-string fallback, symbol/name/logoURI default to empty string, chainId set
from the outer scope or 0 if missing — and return only the mapped properties to
ensure our contract and types are enforced.

}

export async function getTokensByChainName(
chainName: string,
chainId: SupportedChainId
): Promise<TokenFromAPI[]> {
return fetchTokensFromCoinGecko(chainName, chainId);
}
1 change: 1 addition & 0 deletions libs/repositories/src/datasources/tokenSearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './tokenList';
Loading