diff --git a/README.md b/README.md index 6157810..a0c6218 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,16 @@ yarn add @mento-protocol/mento-sdk You can find example usages of the SDK in the [mento-sdk-examples](https://github.com/mento-protocol/mento-sdk-examples) repository. For in-depth documentation and walk through explanations please see the [SDK section](https://docs.mento.org/mento/developers/mento-sdk) of the Mento docs. -## Tradable Pairs Cache +## Tokens & Tradable Pairs Cache -Anytime we launch a new stable token, we need to update the tradable pairs cache. +Anytime we launch a new stable token, we need to update the tokens & tradable pairs caches. -The `yarn cacheTradablePairs` script generates a TypeScript file containing a list of all tradable pairs on the Mento protocol. This file is used to cache the tradable pairs in the SDK and avoid costly re-fetching from the network. - -```sh -yarn cacheTradablePairs -``` +- The `yarn cacheTokens` script generates a TypeScript file containing a list of all tradable Tokens on the Mento protocol. This cache can be used by UIs to avoid costly async token data lookups. +- The `yarn cacheTradablePairs` script generates a TypeScript file containing a list of all tradable pairs on the Mento protocol. This file is used to cache the tradable pairs in the SDK and avoid costly re-fetching from the network. ## Token Graph Visualization -Current token connectivity on Celo Mainnet (last updated: 2025-09-19): +Current token connectivity on Celo Mainnet (last updated: 2025-10-02): ```mermaid graph TD @@ -52,7 +49,6 @@ graph TD cNGN --- cUSD cKES --- cUSD cUSD --- eXOF - ``` **Network Stats:** 20 tokens, 19 direct trading pairs diff --git a/package.json b/package.json index 164edcf..8d918c7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mento-protocol/mento-sdk", "description": "Official SDK for interacting with the Mento Protocol", - "version": "1.12.0", + "version": "1.15.4", "license": "MIT", "author": "Mento Labs", "keywords": [ @@ -33,6 +33,7 @@ "test": "jest --runInBand --verbose", "coverage": "jest --coverage", "cacheTradablePairs": "ts-node scripts/cacheTradablePairs.ts", + "cacheTokens": "ts-node scripts/cacheTokens/index.ts", "printTradablePairs": "ts-node scripts/printTradablePairs.ts", "tradingLimits": "ts-node scripts/printTradingLimits.ts", "limits": "yarn tradingLimits", diff --git a/scripts/cacheTokens/README.md b/scripts/cacheTokens/README.md new file mode 100644 index 0000000..e8c81a1 --- /dev/null +++ b/scripts/cacheTokens/README.md @@ -0,0 +1,89 @@ +# Token Caching Script + +This script fetches token metadata from the blockchain for all unique tokens available on Mento Protocol and caches them in static TypeScript files. It dynamically generates a type-safe TokenSymbol enum, address mappings, and helper functions. + +## Purpose + +- **Synchronous Access**: Enables synchronous access to token data without async blockchain calls +- **Type Safety**: Dynamically generates type-safe `TokenSymbol` enum and `TOKEN_ADDRESSES_BY_CHAIN` mapping +- **Performance**: Eliminates network calls for token metadata +- **Offline Support**: Token data available without network connection +- **Zero Hardcoding**: Everything auto-generated from blockchain data + +## Usage + +### Cache tokens for all supported chains + +```bash +yarn cacheTokens +``` + +### Cache tokens for specific chains + +```bash +yarn cacheTokens --chain-ids=42220,44787 +``` + +## Output + +The script generates TypeScript files in `src/constants/`: + +**Individual token files (per chain):** + +- `tokens.42220.ts` - Celo Mainnet tokens (readonly Token[]) +- `tokens.44787.ts` - Alfajores Testnet tokens (readonly Token[]) +- `tokens.11142220.ts` - Celo Sepolia Testnet tokens (readonly Token[]) + +**Main index file (dynamically generated):** + +- `tokens.ts` - Contains: + - `TokenSymbol` enum - All unique token symbols across all chains + - `TOKEN_ADDRESSES_BY_CHAIN` - Address mapping by chain and symbol + - `getCachedTokens()` - Async token loading function + - `getCachedTokensSync()` - Synchronous token loading function + - `getTokenAddress()` - Helper to get token address by symbol + - `findTokenBySymbol()` - Helper to find token by symbol + +## What Gets Generated + +Everything is computed dynamically from blockchain data: + +✅ **Token enum** - Unique symbols across all chains (e.g., `TokenSymbol.CELO`, `TokenSymbol.cUSD`) +✅ **Address mappings** - Complete `TOKEN_ADDRESSES_BY_CHAIN` for all chains +✅ **Helper functions** - Type-safe address lookups and token search +✅ **Chain support** - Auto-detects supported chains from network config +✅ **Error messages** - Dynamic list of supported chains in error messages + +## Configuration + +RPC URLs can be configured via environment variables: + +- `CELO_RPC_URL` - Celo Mainnet RPC URL (default: ) +- `ALFAJORES_RPC_URL` - Alfajores RPC URL (default: ) +- `CELO_SEPOLIA_RPC_URL` - Celo Sepolia RPC URL (default: ) + +## When to Regenerate + +Regenerate cached tokens when: + +- New tokens are added to the Mento Protocol +- Existing tokens are removed from the Mento Protocol +- Token metadata changes (symbol, name, decimals) +- New chains are added to the protocol + +## Example Output + +```bash +📡 Cache tokens for chain(s): 42220, 44787, 11142220 +🔄 Generating tokens for chain 42220... +✅ Successfully cached 20 tokens to tokens.42220.ts + +🔄 Generating tokens for chain 44787... +✅ Successfully cached 20 tokens to tokens.44787.ts + +🔄 Generating tokens for chain 11142220... +✅ Successfully cached 20 tokens to tokens.11142220.ts + +🔄 Generating tokens.ts index file... +✅ Generated tokens.ts with 23 unique token symbols +``` diff --git a/scripts/cacheTokens/cli.ts b/scripts/cacheTokens/cli.ts new file mode 100644 index 0000000..31026e2 --- /dev/null +++ b/scripts/cacheTokens/cli.ts @@ -0,0 +1,34 @@ +import { SupportedChainId } from '../shared/network' + +export interface CommandLineArgs { + targetChainIds?: SupportedChainId[] +} + +/** + * Parse command line arguments for token caching script + */ +export function parseCommandLineArgs(): CommandLineArgs { + const args: CommandLineArgs = {} + + // Check if specific chain IDs were requested + const chainIdArg = process.argv.find((arg) => arg.startsWith('--chain-ids=')) + if (chainIdArg) { + const chainIdsStr = chainIdArg.split('=')[1] + args.targetChainIds = chainIdsStr + .split(',') + .map((id) => parseInt(id.trim(), 10) as SupportedChainId) + } + + return args +} + +/** + * Print usage tips for the token caching script + */ +export function printUsageTips(): void { + console.log('\n💡 Usage tips:') + console.log(' - To cache tokens for specific chains:') + console.log(' yarn cacheTokens --chain-ids=42220,44787') + console.log(' - To cache tokens for all chains:') + console.log(' yarn cacheTokens') +} diff --git a/scripts/cacheTokens/fileGenerator.ts b/scripts/cacheTokens/fileGenerator.ts new file mode 100644 index 0000000..a5c3d68 --- /dev/null +++ b/scripts/cacheTokens/fileGenerator.ts @@ -0,0 +1,65 @@ +import * as fs from 'fs' +import * as path from 'path' +import { Token } from '../../src/mento' +import { SupportedChainId } from '../shared/network' + +/** + * Generate TypeScript file content with token data + */ +export function generateFileContent( + chainId: SupportedChainId, + tokens: Token[] +): string { + const now = new Date().toISOString() + + // Generate token entries with TokenSymbol enum references + const tokenEntries = tokens + .map((token) => { + // Create a safe enum key (replace special characters) + const safeKey = token.symbol.replace(/[^a-zA-Z0-9_]/g, '_') + + return ` { + address: '${token.address}', + symbol: TokenSymbol.${safeKey}, + name: '${token.name}', + decimals: ${token.decimals}, + }` + }) + .join(',\n') + + const content = `// This file is auto-generated. Do not edit manually. +// Generated on ${now} + +import { Token, TokenSymbol } from '../mento' + +export const tokens${chainId}: readonly Token[] = [ +${tokenEntries}, +] as const +` + + return content +} + +/** + * Write the generated content to a TypeScript file + */ +export function writeToFile( + chainId: SupportedChainId, + content: string, + scriptDir: string +): string { + // Determine the output directory (src/constants/) + const outputDir = path.resolve(scriptDir, `${process.cwd()}/src/constants`) + + // Ensure the output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + // Write to file + const fileName = `tokens.${chainId}.ts` + const filePath = path.join(outputDir, fileName) + fs.writeFileSync(filePath, content) + + return fileName +} diff --git a/scripts/cacheTokens/index.ts b/scripts/cacheTokens/index.ts new file mode 100644 index 0000000..8795f30 --- /dev/null +++ b/scripts/cacheTokens/index.ts @@ -0,0 +1,85 @@ +import { providers } from 'ethers' +import { Mento, Token } from '../../src/mento' +import { rpcUrls, SupportedChainId } from '../shared/network' +import { parseCommandLineArgs, printUsageTips } from './cli' +import { generateFileContent, writeToFile } from './fileGenerator' +import { generateTokensIndexFile } from './tokensIndexGenerator' + +/** + * Fetch tokens for a specific chain + */ +async function fetchTokensForChain( + chainId: SupportedChainId +): Promise { + const provider = new providers.JsonRpcProvider(rpcUrls[chainId]) + const mento = await Mento.create(provider) + + console.log(`📡 Fetching tokens from blockchain...`) + const tokens = await mento.getTokensAsync({ cached: false }) + console.log(`✅ Fetched ${tokens.length} unique tokens`) + + return tokens +} + +/** + * Generate and cache tokens for a specific chain + */ +async function generateAndCacheTokens( + chainId: SupportedChainId +): Promise { + const tokens = await fetchTokensForChain(chainId) + + // Generate and write the TypeScript file + const content = generateFileContent(chainId, tokens) + const fileName = writeToFile(chainId, content, __dirname) + + console.log( + `\n✅ Successfully cached ${tokens.length} tokens to ${fileName}\n` + ) + + return tokens +} + +/** + * Main function that orchestrates the entire caching process + */ +export async function main(): Promise { + const args = parseCommandLineArgs() + + // Determine which chain IDs to process + const chainIdsToProcess = + args.targetChainIds || + (Object.keys(rpcUrls).map(Number) as SupportedChainId[]) + + console.log(`📡 Cache tokens for chain(s): ${chainIdsToProcess.join(', ')}`) + + // Store tokens by chain for index file generation + const tokensByChain: { [chainId: number]: Token[] } = {} + + // Generate tokens for specified chain IDs + for (const chainId of chainIdsToProcess) { + console.log(`\n🔄 \x1b[1mGenerating tokens for chain ${chainId}...\x1b[0m`) + try { + const tokens = await generateAndCacheTokens(chainId as SupportedChainId) + tokensByChain[chainId] = tokens + } catch (error) { + console.error(`❌ Error generating tokens for chain ${chainId}:`, error) + } + } + + // Generate the main tokens.ts index file with enums and mappings + console.log(`\n🔄 \x1b[1mGenerating tokens.ts index file...\x1b[0m`) + generateTokensIndexFile(tokensByChain, __dirname) + + printUsageTips() +} + +// Run the script if called directly +if (require.main === module) { + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +} diff --git a/scripts/cacheTokens/tokensIndexGenerator.ts b/scripts/cacheTokens/tokensIndexGenerator.ts new file mode 100644 index 0000000..6840646 --- /dev/null +++ b/scripts/cacheTokens/tokensIndexGenerator.ts @@ -0,0 +1,161 @@ +import * as fs from 'fs' +import * as path from 'path' +import { Token } from '../../src/mento' + +interface TokensByChain { + [chainId: number]: Token[] +} + +/** + * Generate the main tokens.ts file with enums and helper functions + * This file is dynamically generated from the cached token data + */ +export function generateTokensIndexFile( + tokensByChain: TokensByChain, + scriptDir: string +): void { + // Collect all unique token symbols across all chains + const allSymbols = new Set() + const tokenAddressesByChain: { + [chainId: number]: { [symbol: string]: string } + } = {} + + for (const [chainId, tokens] of Object.entries(tokensByChain)) { + tokenAddressesByChain[Number(chainId)] = {} + + tokens.forEach((token: Token) => { + allSymbols.add(token.symbol) + tokenAddressesByChain[Number(chainId)][token.symbol] = token.address + }) + } + + // Sort symbols alphabetically for consistent output + const sortedSymbols = Array.from(allSymbols).sort() + + // Generate enum entries + const enumEntries = sortedSymbols + .map((symbol) => { + // Create a safe key for enum (replace special characters) + const safeKey = symbol.replace(/[^a-zA-Z0-9_]/g, '_') + return ` ${safeKey} = '${symbol}',` + }) + .join('\n') + + // Generate chain case statements for getCachedTokens + const getCachedTokensCases = Object.keys(tokensByChain) + .map((chainId) => { + return ` case ${chainId}: + return await import('./tokens.${chainId}').then( + (module) => module.tokens${chainId} + )` + }) + .join('\n') + + // Generate chain case statements for getCachedTokensSync + const getCachedTokensSyncCases = Object.keys(tokensByChain) + .map((chainId) => { + return ` case ${chainId}: + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./tokens.${chainId}').tokens${chainId}` + }) + .join('\n') + + // Generate supported chains list for error messages + const supportedChainsList = Object.keys(tokensByChain) + .map((chainId) => { + const names: { [key: string]: string } = { + '42220': 'Celo', + '44787': 'Alfajores', + '11142220': 'Celo Sepolia', + } + return `${chainId} (${names[chainId] || 'Unknown'})` + }) + .join(', ') + + // Generate TOKEN_ADDRESSES_BY_CHAIN mapping + const tokenAddressesMapping = Object.entries(tokenAddressesByChain) + .map(([chainId, tokens]) => { + const entries = Object.entries(tokens) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([symbol, address]) => { + // Use quoted key if symbol contains special characters + const needsQuotes = /[^a-zA-Z0-9_$]/.test(symbol) + const key = needsQuotes ? `'${symbol}'` : symbol + return ` ${key}: '${address}',` + }) + .join('\n') + + return ` ${chainId}: {\n${entries}\n },` + }) + .join('\n') + + const now = new Date().toISOString() + + const content = `// This file is auto-generated. Do not edit manually. +// Generated on ${now} + +import { Token } from '../mento' + +/** + * Gets cached tokens for a specific chain ID + * @param chainId - The chain ID to get cached tokens for + * @returns Promise resolving to the cached tokens or undefined if not available + */ +export async function getCachedTokens( + chainId: number +): Promise { + switch (chainId) { +${getCachedTokensCases} + default: + return undefined + } +} + +/** + * Synchronously gets cached tokens for a specific chain ID + * Note: This function throws if no cached tokens are available. + * Use getCachedTokens() for async loading or when you want to handle missing cache gracefully. + * + * @param chainId - The chain ID to get cached tokens for + * @returns The cached tokens + * @throws Error if no cached tokens are available for the chain + */ +export function getCachedTokensSync(chainId: number): readonly Token[] { + switch (chainId) { +${getCachedTokensSyncCases} + default: + throw new Error( + \`No cached tokens available for chain ID \${chainId}. \` + + \`Supported chains: ${supportedChainsList}\` + ) + } +} + +/** + * Token symbol enum for type-safe access across all chains + * Note: Not all tokens are available on all chains - check TOKEN_ADDRESSES_BY_CHAIN + */ +export enum TokenSymbol { +${enumEntries} +} + +/** + * Token addresses mapped by chain ID and symbol + * Use this for type-safe token address lookups + */ +export const TOKEN_ADDRESSES_BY_CHAIN: { + [chainId: number]: Partial> +} = { +${tokenAddressesMapping} +} +` + + // Write the tokens.ts file to src/constants/ + const outputDir = path.resolve(scriptDir, `${process.cwd()}/src/constants`) + const filePath = path.join(outputDir, 'tokens.ts') + fs.writeFileSync(filePath, content) + + console.log( + `✅ Generated tokens.ts with ${sortedSymbols.length} unique token symbols` + ) +} diff --git a/scripts/tokens/index.ts b/scripts/tokens/index.ts index 4d0100d..0c66959 100644 --- a/scripts/tokens/index.ts +++ b/scripts/tokens/index.ts @@ -23,7 +23,7 @@ async function fetchTokensForNetwork( ): Promise { const provider = new ethers.providers.JsonRpcProvider(rpcUrl) const mento = await Mento.create(provider) - const tokens = await mento.getTokens() + const tokens = await mento.getTokensAsync() return tokens.map((token) => ({ ...token, @@ -103,7 +103,7 @@ async function main(): Promise { text: 'Fetching tokens...', color: 'cyan', }).start() - const tokens = await mento.getTokens() + const tokens = await mento.getTokensAsync() tokensSpinner.succeed(`Fetched ${tokens.length} unique tokens`) // Find network name from chainId diff --git a/scripts/visualizeTokenGraph.ts b/scripts/visualizeTokenGraph.ts index dbd0b1c..edc3b29 100644 --- a/scripts/visualizeTokenGraph.ts +++ b/scripts/visualizeTokenGraph.ts @@ -73,8 +73,8 @@ function getChainName(chainId: number): string { return 'Celo Mainnet' case 44787: return 'Alfajores Testnet' - case 62320: - return 'Baklava Testnet' + case 11142220: + return 'Celo Sepolia Testnet' default: return `Chain ${chainId}` } diff --git a/src/constants/index.ts b/src/constants/index.ts index 8953fdf..7973cb5 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,5 @@ export * from './addresses' export * from './currencies' +export * from './tokens' export * from './tradablePairs' + diff --git a/src/constants/tokens.11142220.ts b/src/constants/tokens.11142220.ts new file mode 100644 index 0000000..9be1c2c --- /dev/null +++ b/src/constants/tokens.11142220.ts @@ -0,0 +1,127 @@ +// This file is auto-generated. Do not edit manually. +// Generated on 2025-10-06T16:08:43.517Z + +import { Token, TokenSymbol } from '../mento' + +export const tokens11142220: readonly Token[] = [ + { + address: '0x9883d788d40F1C7595a780ed881Ea833C7743B4B', + symbol: TokenSymbol.axlEUROC, + name: 'Mento Mock axlEUROC', + decimals: 18, + }, + { + address: '0x6285De9DA7C1d329C0451628638908915002d9d1', + symbol: TokenSymbol.axlUSDC, + name: 'Mento Mock axlUSDC', + decimals: 18, + }, + { + address: '0x5873Faeb42F3563dcD77F0fbbdA818E6d6DA3139', + symbol: TokenSymbol.cAUD, + name: 'Celo Australian Dollar', + decimals: 18, + }, + { + address: '0xF151c9a13b78C84f93f50B8b3bC689fedc134F60', + symbol: TokenSymbol.cCAD, + name: 'Celo Canadian Dollar', + decimals: 18, + }, + { + address: '0x284E9b7B623eAE866914b7FA0eB720C2Bb3C2980', + symbol: TokenSymbol.cCHF, + name: 'Celo Swiss Franc', + decimals: 18, + }, + { + address: '0x5F8d55c3627d2dc0a2B4afa798f877242F382F67', + symbol: TokenSymbol.cCOP, + name: 'Celo Colombian Peso', + decimals: 18, + }, + { + address: '0x471EcE3750Da237f93B8E339c536989b8978a438', + symbol: TokenSymbol.CELO, + name: 'Celo native asset', + decimals: 18, + }, + { + address: '0xA99dC247d6b7B2E3ab48a1fEE101b83cD6aCd82a', + symbol: TokenSymbol.cEUR, + name: 'Celo Euro', + decimals: 18, + }, + { + address: '0x85F5181Abdbf0e1814Fc4358582Ae07b8eBA3aF3', + symbol: TokenSymbol.cGBP, + name: 'Celo British Pound', + decimals: 18, + }, + { + address: '0x5e94B8C872bD47BC4255E60ECBF44D5E66e7401C', + symbol: TokenSymbol.cGHS, + name: 'Celo Ghanaian Cedi', + decimals: 18, + }, + { + address: '0x85Bee67D435A39f7467a8a9DE34a5B73D25Df426', + symbol: TokenSymbol.cJPY, + name: 'Celo Japanese Yen', + decimals: 18, + }, + { + address: '0xC7e4635651E3e3Af82b61d3E23c159438daE3BbF', + symbol: TokenSymbol.cKES, + name: 'Celo Kenyan Shilling', + decimals: 18, + }, + { + address: '0x3d5ae86F34E2a82771496D140daFAEf3789dF888', + symbol: TokenSymbol.cNGN, + name: 'Celo Nigerian Naira', + decimals: 18, + }, + { + address: '0x2294298942fdc79417DE9E0D740A4957E0e7783a', + symbol: TokenSymbol.cREAL, + name: 'Celo Brazilian Real', + decimals: 18, + }, + { + address: '0xdE9e4C3ce781b4bA68120d6261cbad65ce0aB00b', + symbol: TokenSymbol.cUSD, + name: 'Celo Dollar', + decimals: 18, + }, + { + address: '0x10CCfB235b0E1Ed394bACE4560C3ed016697687e', + symbol: TokenSymbol.cZAR, + name: 'Celo South African Rand', + decimals: 18, + }, + { + address: '0x5505b70207aE3B826c1A7607F19F3Bf73444A082', + symbol: TokenSymbol.eXOF, + name: 'ECO CFA', + decimals: 18, + }, + { + address: '0x0352976d940a2C3FBa0C3623198947Ee1d17869E', + symbol: TokenSymbol.PUSO, + name: 'PUSO', + decimals: 18, + }, + { + address: '0xBD63e46Be8eF8D89dFde3054E7b9ECAEb8Ad83e9', + symbol: TokenSymbol.USDC, + name: 'Mento Mock USDC', + decimals: 18, + }, + { + address: '0xCA53d9b72646B254d29EBeEb4c5cde7BB4bb59e0', + symbol: TokenSymbol.USDT, + name: 'Mento Mock USDT', + decimals: 18, + }, +] as const diff --git a/src/constants/tokens.42220.ts b/src/constants/tokens.42220.ts new file mode 100644 index 0000000..f5071ed --- /dev/null +++ b/src/constants/tokens.42220.ts @@ -0,0 +1,127 @@ +// This file is auto-generated. Do not edit manually. +// Generated on 2025-10-06T16:08:23.483Z + +import { Token, TokenSymbol } from '../mento' + +export const tokens42220: readonly Token[] = [ + { + address: '0x061cc5a2C863E0C1Cb404006D559dB18A34C762d', + symbol: TokenSymbol.axlEUROC, + name: 'Axelar Wrapped EUROC', + decimals: 6, + }, + { + address: '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + symbol: TokenSymbol.axlUSDC, + name: 'Axelar Wrapped USDC', + decimals: 6, + }, + { + address: '0x7175504C455076F15c04A2F90a8e352281F492F9', + symbol: TokenSymbol.cAUD, + name: 'Celo Australian Dollar', + decimals: 18, + }, + { + address: '0xff4Ab19391af240c311c54200a492233052B6325', + symbol: TokenSymbol.cCAD, + name: 'Celo Canadian Dollar', + decimals: 18, + }, + { + address: '0xb55a79F398E759E43C95b979163f30eC87Ee131D', + symbol: TokenSymbol.cCHF, + name: 'Celo Swiss Franc', + decimals: 18, + }, + { + address: '0x8A567e2aE79CA692Bd748aB832081C45de4041eA', + symbol: TokenSymbol.cCOP, + name: 'Celo Colombian Peso', + decimals: 18, + }, + { + address: '0x471EcE3750Da237f93B8E339c536989b8978a438', + symbol: TokenSymbol.CELO, + name: 'Celo native asset', + decimals: 18, + }, + { + address: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73', + symbol: TokenSymbol.cEUR, + name: 'Celo Euro', + decimals: 18, + }, + { + address: '0xCCF663b1fF11028f0b19058d0f7B674004a40746', + symbol: TokenSymbol.cGBP, + name: 'Celo British Pound', + decimals: 18, + }, + { + address: '0xfAeA5F3404bbA20D3cc2f8C4B0A888F55a3c7313', + symbol: TokenSymbol.cGHS, + name: 'Celo Ghanaian Cedi', + decimals: 18, + }, + { + address: '0xc45eCF20f3CD864B32D9794d6f76814aE8892e20', + symbol: TokenSymbol.cJPY, + name: 'Celo Japanese Yen', + decimals: 18, + }, + { + address: '0x456a3D042C0DbD3db53D5489e98dFb038553B0d0', + symbol: TokenSymbol.cKES, + name: 'Celo Kenyan Shilling', + decimals: 18, + }, + { + address: '0xE2702Bd97ee33c88c8f6f92DA3B733608aa76F71', + symbol: TokenSymbol.cNGN, + name: 'Celo Nigerian Naira', + decimals: 18, + }, + { + address: '0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787', + symbol: TokenSymbol.cREAL, + name: 'Celo Brazilian Real', + decimals: 18, + }, + { + address: '0x765DE816845861e75A25fCA122bb6898B8B1282a', + symbol: TokenSymbol.cUSD, + name: 'Celo Dollar', + decimals: 18, + }, + { + address: '0x4c35853A3B4e647fD266f4de678dCc8fEC410BF6', + symbol: TokenSymbol.cZAR, + name: 'Celo South African Rand', + decimals: 18, + }, + { + address: '0x73F93dcc49cB8A239e2032663e9475dd5ef29A08', + symbol: TokenSymbol.eXOF, + name: 'ECO CFA', + decimals: 18, + }, + { + address: '0x105d4A9306D2E55a71d2Eb95B81553AE1dC20d7B', + symbol: TokenSymbol.PUSO, + name: 'PUSO', + decimals: 18, + }, + { + address: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', + symbol: TokenSymbol.USD_, + name: 'Tether USD', + decimals: 6, + }, + { + address: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C', + symbol: TokenSymbol.USDC, + name: 'USDC', + decimals: 6, + }, +] as const diff --git a/src/constants/tokens.44787.ts b/src/constants/tokens.44787.ts new file mode 100644 index 0000000..9c1e441 --- /dev/null +++ b/src/constants/tokens.44787.ts @@ -0,0 +1,127 @@ +// This file is auto-generated. Do not edit manually. +// Generated on 2025-10-06T16:08:33.900Z + +import { Token, TokenSymbol } from '../mento' + +export const tokens44787: readonly Token[] = [ + { + address: '0x6e673502c5b55F3169657C004e5797fFE5be6653', + symbol: TokenSymbol.BridgedEUROC, + name: 'mockBridgedEUROC', + decimals: 6, + }, + { + address: '0x87D61dA3d668797786D73BC674F053f87111570d', + symbol: TokenSymbol.BridgedUSDC, + name: 'mockBridgedUSDC', + decimals: 6, + }, + { + address: '0x84CBD49F5aE07632B6B88094E81Cce8236125Fe0', + symbol: TokenSymbol.cAUD, + name: 'Celo Australian Dollar', + decimals: 18, + }, + { + address: '0x02EC9E0D2Fd73e89168C1709e542a48f58d7B133', + symbol: TokenSymbol.cCAD, + name: 'Celo Canadian Dollar', + decimals: 18, + }, + { + address: '0xADC57C2C34aD021Df4421230a6532F4e2E1dCE4F', + symbol: TokenSymbol.cCHF, + name: 'Celo Swiss Franc', + decimals: 18, + }, + { + address: '0xe6A57340f0df6E020c1c0a80bC6E13048601f0d4', + symbol: TokenSymbol.cCOP, + name: 'Celo Colombian Peso', + decimals: 18, + }, + { + address: '0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9', + symbol: TokenSymbol.CELO, + name: 'Celo native asset', + decimals: 18, + }, + { + address: '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', + symbol: TokenSymbol.cEUR, + name: 'Celo Euro', + decimals: 18, + }, + { + address: '0x47f2Fb88105155a18c390641C8a73f1402B2BB12', + symbol: TokenSymbol.cGBP, + name: 'Celo British Pound', + decimals: 18, + }, + { + address: '0x295B66bE7714458Af45E6A6Ea142A5358A6cA375', + symbol: TokenSymbol.cGHS, + name: 'cGHS', + decimals: 18, + }, + { + address: '0x2E51F41238cA36a421C9B8b3e189e8Cc7653FE67', + symbol: TokenSymbol.cJPY, + name: 'Celo Japanese Yen', + decimals: 18, + }, + { + address: '0x1E0433C1769271ECcF4CFF9FDdD515eefE6CdF92', + symbol: TokenSymbol.cKES, + name: 'Celo Kenyan Shilling', + decimals: 18, + }, + { + address: '0x4a5b03B8b16122D330306c65e4CA4BC5Dd6511d0', + symbol: TokenSymbol.cNGN, + name: 'Celo Nigerian Naira', + decimals: 18, + }, + { + address: '0xE4D517785D091D3c54818832dB6094bcc2744545', + symbol: TokenSymbol.cREAL, + name: 'Celo Brazilian Real', + decimals: 18, + }, + { + address: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', + symbol: TokenSymbol.cUSD, + name: 'Celo Dollar', + decimals: 18, + }, + { + address: '0x1e5b44015Ff90610b54000DAad31C89b3284df4d', + symbol: TokenSymbol.cZAR, + name: 'Celo South African Rand', + decimals: 18, + }, + { + address: '0xB0FA15e002516d0301884059c0aaC0F0C72b019D', + symbol: TokenSymbol.eXOF, + name: 'ECO CFA', + decimals: 18, + }, + { + address: '0x5E0E3c9419C42a1B04e2525991FB1A2C467AB8bF', + symbol: TokenSymbol.PUSO, + name: 'PUSO', + decimals: 18, + }, + { + address: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B', + symbol: TokenSymbol.USDC, + name: 'USDC', + decimals: 6, + }, + { + address: '0xBba91F588d031469ABCCA566FE80fB1Ad8Ee3287', + symbol: TokenSymbol.USDT, + name: 'mockNativeUSDT', + decimals: 6, + }, +] as const diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts new file mode 100644 index 0000000..b982fd0 --- /dev/null +++ b/src/constants/tokens.ts @@ -0,0 +1,163 @@ +// This file is auto-generated. Do not edit manually. +// Generated on 2025-10-06T16:08:43.519Z + +import { Token } from '../mento' + +/** + * Gets cached tokens for a specific chain ID + * @param chainId - The chain ID to get cached tokens for + * @returns Promise resolving to the cached tokens or undefined if not available + */ +export async function getCachedTokens( + chainId: number +): Promise { + switch (chainId) { + case 42220: + return await import('./tokens.42220').then( + (module) => module.tokens42220 + ) + case 44787: + return await import('./tokens.44787').then( + (module) => module.tokens44787 + ) + case 11142220: + return await import('./tokens.11142220').then( + (module) => module.tokens11142220 + ) + default: + return undefined + } +} + +/** + * Synchronously gets cached tokens for a specific chain ID + * Note: This function throws if no cached tokens are available. + * Use getCachedTokens() for async loading or when you want to handle missing cache gracefully. + * + * @param chainId - The chain ID to get cached tokens for + * @returns The cached tokens + * @throws Error if no cached tokens are available for the chain + */ +export function getCachedTokensSync(chainId: number): readonly Token[] { + switch (chainId) { + case 42220: + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./tokens.42220').tokens42220 + case 44787: + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./tokens.44787').tokens44787 + case 11142220: + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require('./tokens.11142220').tokens11142220 + default: + throw new Error( + `No cached tokens available for chain ID ${chainId}. ` + + `Supported chains: 42220 (Celo), 44787 (Alfajores), 11142220 (Celo Sepolia)` + ) + } +} + +/** + * Token symbol enum for type-safe access across all chains + * Note: Not all tokens are available on all chains - check TOKEN_ADDRESSES_BY_CHAIN + */ +export enum TokenSymbol { + BridgedEUROC = 'BridgedEUROC', + BridgedUSDC = 'BridgedUSDC', + CELO = 'CELO', + PUSO = 'PUSO', + USDC = 'USDC', + USDT = 'USDT', + USD_ = 'USD₮', + axlEUROC = 'axlEUROC', + axlUSDC = 'axlUSDC', + cAUD = 'cAUD', + cCAD = 'cCAD', + cCHF = 'cCHF', + cCOP = 'cCOP', + cEUR = 'cEUR', + cGBP = 'cGBP', + cGHS = 'cGHS', + cJPY = 'cJPY', + cKES = 'cKES', + cNGN = 'cNGN', + cREAL = 'cREAL', + cUSD = 'cUSD', + cZAR = 'cZAR', + eXOF = 'eXOF', +} + +/** + * Token addresses mapped by chain ID and symbol + * Use this for type-safe token address lookups + */ +export const TOKEN_ADDRESSES_BY_CHAIN: { + [chainId: number]: Partial> +} = { + 42220: { + axlEUROC: '0x061cc5a2C863E0C1Cb404006D559dB18A34C762d', + axlUSDC: '0xEB466342C4d449BC9f53A865D5Cb90586f405215', + cAUD: '0x7175504C455076F15c04A2F90a8e352281F492F9', + cCAD: '0xff4Ab19391af240c311c54200a492233052B6325', + cCHF: '0xb55a79F398E759E43C95b979163f30eC87Ee131D', + cCOP: '0x8A567e2aE79CA692Bd748aB832081C45de4041eA', + CELO: '0x471EcE3750Da237f93B8E339c536989b8978a438', + cEUR: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73', + cGBP: '0xCCF663b1fF11028f0b19058d0f7B674004a40746', + cGHS: '0xfAeA5F3404bbA20D3cc2f8C4B0A888F55a3c7313', + cJPY: '0xc45eCF20f3CD864B32D9794d6f76814aE8892e20', + cKES: '0x456a3D042C0DbD3db53D5489e98dFb038553B0d0', + cNGN: '0xE2702Bd97ee33c88c8f6f92DA3B733608aa76F71', + cREAL: '0xe8537a3d056DA446677B9E9d6c5dB704EaAb4787', + cUSD: '0x765DE816845861e75A25fCA122bb6898B8B1282a', + cZAR: '0x4c35853A3B4e647fD266f4de678dCc8fEC410BF6', + eXOF: '0x73F93dcc49cB8A239e2032663e9475dd5ef29A08', + PUSO: '0x105d4A9306D2E55a71d2Eb95B81553AE1dC20d7B', + 'USD₮': '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', + USDC: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C', + }, + 44787: { + BridgedEUROC: '0x6e673502c5b55F3169657C004e5797fFE5be6653', + BridgedUSDC: '0x87D61dA3d668797786D73BC674F053f87111570d', + cAUD: '0x84CBD49F5aE07632B6B88094E81Cce8236125Fe0', + cCAD: '0x02EC9E0D2Fd73e89168C1709e542a48f58d7B133', + cCHF: '0xADC57C2C34aD021Df4421230a6532F4e2E1dCE4F', + cCOP: '0xe6A57340f0df6E020c1c0a80bC6E13048601f0d4', + CELO: '0xF194afDf50B03e69Bd7D057c1Aa9e10c9954E4C9', + cEUR: '0x10c892A6EC43a53E45D0B916B4b7D383B1b78C0F', + cGBP: '0x47f2Fb88105155a18c390641C8a73f1402B2BB12', + cGHS: '0x295B66bE7714458Af45E6A6Ea142A5358A6cA375', + cJPY: '0x2E51F41238cA36a421C9B8b3e189e8Cc7653FE67', + cKES: '0x1E0433C1769271ECcF4CFF9FDdD515eefE6CdF92', + cNGN: '0x4a5b03B8b16122D330306c65e4CA4BC5Dd6511d0', + cREAL: '0xE4D517785D091D3c54818832dB6094bcc2744545', + cUSD: '0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1', + cZAR: '0x1e5b44015Ff90610b54000DAad31C89b3284df4d', + eXOF: '0xB0FA15e002516d0301884059c0aaC0F0C72b019D', + PUSO: '0x5E0E3c9419C42a1B04e2525991FB1A2C467AB8bF', + USDC: '0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B', + USDT: '0xBba91F588d031469ABCCA566FE80fB1Ad8Ee3287', + }, + 11142220: { + axlEUROC: '0x9883d788d40F1C7595a780ed881Ea833C7743B4B', + axlUSDC: '0x6285De9DA7C1d329C0451628638908915002d9d1', + cAUD: '0x5873Faeb42F3563dcD77F0fbbdA818E6d6DA3139', + cCAD: '0xF151c9a13b78C84f93f50B8b3bC689fedc134F60', + cCHF: '0x284E9b7B623eAE866914b7FA0eB720C2Bb3C2980', + cCOP: '0x5F8d55c3627d2dc0a2B4afa798f877242F382F67', + CELO: '0x471EcE3750Da237f93B8E339c536989b8978a438', + cEUR: '0xA99dC247d6b7B2E3ab48a1fEE101b83cD6aCd82a', + cGBP: '0x85F5181Abdbf0e1814Fc4358582Ae07b8eBA3aF3', + cGHS: '0x5e94B8C872bD47BC4255E60ECBF44D5E66e7401C', + cJPY: '0x85Bee67D435A39f7467a8a9DE34a5B73D25Df426', + cKES: '0xC7e4635651E3e3Af82b61d3E23c159438daE3BbF', + cNGN: '0x3d5ae86F34E2a82771496D140daFAEf3789dF888', + cREAL: '0x2294298942fdc79417DE9E0D740A4957E0e7783a', + cUSD: '0xdE9e4C3ce781b4bA68120d6261cbad65ce0aB00b', + cZAR: '0x10CCfB235b0E1Ed394bACE4560C3ed016697687e', + eXOF: '0x5505b70207aE3B826c1A7607F19F3Bf73444A082', + PUSO: '0x0352976d940a2C3FBa0C3623198947Ee1d17869E', + USDC: '0xBD63e46Be8eF8D89dFde3054E7b9ECAEb8Ad83e9', + USDT: '0xCA53d9b72646B254d29EBeEb4c5cde7BB4bb59e0', + }, +} diff --git a/src/mento.test.ts b/src/mento.test.ts index b621585..19d9912 100644 --- a/src/mento.test.ts +++ b/src/mento.test.ts @@ -1033,10 +1033,46 @@ describe('Mento', () => { }) describe('getTokens', () => { + it('should return cached tokens synchronously after SDK is initialized', async () => { + const testee = await Mento.create(provider) + + const tokens = testee.getTokens() + + // Verify we get tokens from the cache + expect(tokens.length).toBeGreaterThan(0) + + // Verify each token has the expected structure + tokens.forEach((token) => { + expect(token).toHaveProperty('address') + expect(token).toHaveProperty('symbol') + expect(token).toHaveProperty('name') + expect(token).toHaveProperty('decimals') + expect(typeof token.address).toBe('string') + expect(typeof token.symbol).toBe('string') + expect(typeof token.name).toBe('string') + expect(typeof token.decimals).toBe('number') + }) + }) + + it('should throw when chainId is not initialized', () => { + // Create instance using createWithParams which doesn't initialize cachedChainId + const testee = Mento.createWithParams( + provider, + constants.AddressZero, + [] + ) + + expect(() => testee.getTokens()).toThrow( + 'Chain ID not yet initialized' + ) + }) + }) + + describe('getTokensAsync', () => { it('should return a list of unique tokens sorted by symbol', async () => { const testee = await Mento.create(provider) - const tokens = await testee.getTokens({ cached: false }) + const tokens = await testee.getTokensAsync({ cached: false }) // We should have exactly 4 unique tokens from our test data expect(tokens.length).toBe(4) @@ -1061,7 +1097,7 @@ describe('Mento', () => { it('should not have duplicate tokens', async () => { const testee = await Mento.create(provider) - const tokens = await testee.getTokens({ cached: false }) + const tokens = await testee.getTokensAsync({ cached: false }) // Check that all addresses are unique const addresses = tokens.map((t) => t.address) @@ -1073,5 +1109,20 @@ describe('Mento', () => { const uniqueSymbols = new Set(symbols) expect(symbols.length).toBe(uniqueSymbols.size) }) + + it('should return cached tokens when cached is true', async () => { + const testee = await Mento.create(provider) + + const tokens = await testee.getTokensAsync({ cached: true }) + + // Should return cached tokens + expect(tokens.length).toBeGreaterThan(0) + tokens.forEach((token) => { + expect(token).toHaveProperty('address') + expect(token).toHaveProperty('symbol') + expect(token).toHaveProperty('name') + expect(token).toHaveProperty('decimals') + }) + }) }) }) diff --git a/src/mento.ts b/src/mento.ts index 7ecd253..1811ba5 100644 --- a/src/mento.ts +++ b/src/mento.ts @@ -28,6 +28,7 @@ import { import { strict as assert } from 'assert' import { IMentoRouter, IMentoRouter__factory } from 'mento-router-ts' import { getAddress, Identifier } from './constants/addresses' +import { TokenSymbol } from './constants/tokens' import { getCachedTradablePairs, TradablePairWithSpread, @@ -38,6 +39,9 @@ import { selectOptimalRoutes, } from './routeUtils' +// Re-export TokenSymbol for use in auto-generated files and consuming packages +export { TokenSymbol } from './constants/tokens' + export interface Exchange { providerAddr: Address id: string @@ -51,7 +55,7 @@ export interface Asset { export interface Token { address: Address - symbol: string + symbol: TokenSymbol name: string decimals: number } @@ -101,11 +105,14 @@ export class Mento { */ static async create(signerOrProvider: Signer | providers.Provider) { validateSignerOrProvider(signerOrProvider) - return new Mento( + const chainId = await getChainId(signerOrProvider) + const instance = new Mento( signerOrProvider, - getAddress('Broker', await getChainId(signerOrProvider)), - getAddress('MentoRouter', await getChainId(signerOrProvider)) + getAddress('Broker', chainId), + getAddress('MentoRouter', chainId) ) + instance.cachedChainId = chainId + return instance } /** @@ -253,20 +260,53 @@ export class Mento { /** * Returns a list of all unique tokens available on the current chain. - * Each token includes its address, symbol, name, and decimals. + * This method is synchronous and uses pre-cached token data. + * For runtime fetching from the blockchain, use getTokensAsync(). + * + * @returns An array of unique Token objects from the static cache. + * @throws Error if no cached tokens are available for the current chain or if chainId is not yet initialized + */ + getTokens(): Token[] { + if (!this.cachedChainId) { + throw new Error( + 'Chain ID not yet initialized. Use Mento.create() to initialize the SDK, or use getTokensAsync() instead.' + ) + } + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { getCachedTokensSync } = require('./constants/tokens') + return Array.from(getCachedTokensSync(this.cachedChainId)) + } + + /** + * Fetches token metadata from the blockchain at runtime. + * This method is async and makes blockchain calls to get fresh token data. + * For synchronous access using cached data, use getTokens(). + * * @param options - Optional parameters - * @param options.cached - Whether to use cached data (default: true) - * @returns An array of unique Token objects. + * @param options.cached - Whether to use cached data (default: true). + * If true, attempts to load from static cache first. + * @returns A Promise resolving to an array of unique Token objects. */ - async getTokens({ + async getTokensAsync({ cached = true, }: { cached?: boolean } = {}): Promise { - const tradablePairs = await this.getTradablePairsWithPath({ cached }) + // If cached is true, try to use the static cache first + if (cached) { + const { getCachedTokens } = await import('./constants/tokens') + const chainId = await this.chainId() + const cachedTokens = await getCachedTokens(chainId) + if (cachedTokens) { + return Array.from(cachedTokens) + } + } + + // Fall back to fetching from blockchain + const tradablePairs = await this.getTradablePairsWithPath({ cached: false }) // Collect unique token addresses const uniqueAddresses = new Set
( - tradablePairs.flatMap(pair => pair.assets.map(asset => asset.address)) + tradablePairs.flatMap((pair) => pair.assets.map((asset) => asset.address)) ) // Fetch token metadata for each unique address @@ -864,7 +904,7 @@ export class Mento { } async chainId(): Promise { - if (this.cachedChainId == null) { + if (!this.cachedChainId) { this.cachedChainId = await getChainId(this.signerOrProvider) } return this.cachedChainId diff --git a/src/routeFetching.test.ts b/src/routeFetching.test.ts index 75866a5..810817a 100644 --- a/src/routeFetching.test.ts +++ b/src/routeFetching.test.ts @@ -13,7 +13,7 @@ import { Contract, providers } from 'ethers' import { IMentoRouter__factory } from 'mento-router-ts' import { buildRouteDisplay } from '../scripts/quotes/spread' import { Mento, TradablePair } from './mento' -import { findTokenBySymbol } from './utils' +import { findTokenAddressBySymbolInTradablePairs } from './utils' // Simplified mock setup - only what's actually needed jest.mock('@mento-protocol/mento-core-ts', () => ({ @@ -128,7 +128,7 @@ describe('Route Fetching Logic', () => { for (const [symbol, expectedAddress] of expectedTokens) { const displaySymbol = symbol === 'USDT' ? 'USD₮' : symbol - const foundAddress = findTokenBySymbol(allPairs, displaySymbol) + const foundAddress = findTokenAddressBySymbolInTradablePairs(displaySymbol, allPairs) expect(foundAddress).toBe(expectedAddress) } @@ -195,7 +195,7 @@ describe('Route Fetching Logic', () => { describe('Error Handling', () => { it('should handle non-existent token addresses', () => { const fakeAddress = '0x1234567890123456789012345678901234567890' - const result = findTokenBySymbol(allPairs, fakeAddress) + const result = findTokenAddressBySymbolInTradablePairs(fakeAddress, allPairs) expect(result).toBeNull() }) }) diff --git a/src/utils.ts b/src/utils.ts index 6de6217..ca51d2e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,9 @@ import { BigNumberish, Contract, providers, Signer } from 'ethers' +import { TokenSymbol } from './constants' +import { getCachedTokensSync, TOKEN_ADDRESSES_BY_CHAIN } from './constants/tokens' import { Address } from './interfaces' -import { TradablePair } from './mento' +import { Token, TradablePair } from './mento' /** * Gets the chain ID from a signer or provider @@ -61,7 +63,7 @@ export function validateSignerOrProvider( export async function getSymbolFromTokenAddress( tokenAddr: Address, signerOrProvider: Signer | providers.Provider -): Promise { +): Promise { const erc20Abi = ['function symbol() external view returns (string memory)'] const contract = new Contract(tokenAddr, erc20Abi, signerOrProvider) @@ -130,20 +132,44 @@ export async function increaseAllowance( * @param symbol the token symbol to find (case-insensitive) * @returns the token address if found, null otherwise */ -export function findTokenBySymbol( - pairs: readonly TradablePair[], - symbol: string -): string | null { - for (const pair of pairs) { - for (const asset of pair.assets) { - if (asset.symbol.toLowerCase() === symbol.toLowerCase()) { - return asset.address - } - } - } - return null +export function findTokenAddressBySymbolInTradablePairs( + symbol: TokenSymbol, + pairs: readonly TradablePair[] +): Address | null { + return ( + pairs + .flatMap((pair) => pair.assets) + .find((asset) => asset.symbol.toLowerCase() === symbol.toLowerCase()) + ?.address ?? null + ) } export function capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1) +}/** + * Helper function to get token address by symbol for a specific chain + * @param symbol - The token symbol + * @param chainId - The chain ID + * @returns The token address or undefined if not found + */ + +export function getTokenAddress( + symbol: TokenSymbol, + chainId: number +): string | undefined { + return TOKEN_ADDRESSES_BY_CHAIN[chainId]?.[symbol] +} +/** + * Helper function to find a token by symbol in the cached tokens + * @param symbol - The token symbol to search for + * @param chainId - The chain ID + * @returns The token object or undefined if not found + */ + +export function findTokenBySymbol( + symbol: string, + chainId: number +): Token | undefined { + const tokens = getCachedTokensSync(chainId) + return tokens.find((token) => token.symbol === symbol) }