diff --git a/src/app.js b/src/app.js index 00dcc02..034bffd 100644 --- a/src/app.js +++ b/src/app.js @@ -6,6 +6,7 @@ const corsMiddleware = require('./middleware/cors'); const errorHandler = require('./middleware/errorHandler'); const swapRoutes = require('./routes/swap'); const intentRoutes = require('./routes/intents'); +const tokenRoutes = require('./routes/tokens'); const app = express(); const PORT = process.env.PORT || 3002; @@ -41,6 +42,7 @@ app.get('/health', (req, res) => { // Routes app.use('/', swapRoutes); app.use('/', intentRoutes); +app.use('/tokens', tokenRoutes); // Error handling middleware (must be last) app.use(errorHandler); @@ -56,6 +58,7 @@ if (require.main === module) { console.log( `Supported intents: dustZap, unifiedZap (zapIn, zapOut, rebalance coming soon)` ); + console.log(`🪙 Token endpoints: /tokens/zap/{chainId}, /tokens/chains`); }); } diff --git a/src/config/tokenConfig.js b/src/config/tokenConfig.js index 4e31e25..9270ad6 100644 --- a/src/config/tokenConfig.js +++ b/src/config/tokenConfig.js @@ -51,6 +51,7 @@ const TOKEN_REGISTRY = { name: 'Ethereum', decimals: 18, wrappedVersion: 'WETH', + zapEnabled: true, }, WETH: { type: 'wrapped', @@ -60,6 +61,7 @@ const TOKEN_REGISTRY = { decimals: 18, hasDeposit: true, nativeVersion: 'ETH', + zapEnabled: true, }, USDC: { type: 'erc20', @@ -67,6 +69,7 @@ const TOKEN_REGISTRY = { name: 'USD Coin', address: '0xA0b86a33E6441a8C8C5d56Aa14E4e66E8e6B9E2', decimals: 6, + zapEnabled: true, }, USDT: { type: 'erc20', @@ -74,6 +77,7 @@ const TOKEN_REGISTRY = { name: 'Tether USD', address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', decimals: 6, + zapEnabled: true, }, }, @@ -85,6 +89,7 @@ const TOKEN_REGISTRY = { name: 'Ethereum', decimals: 18, wrappedVersion: 'WETH', + zapEnabled: true, }, WETH: { type: 'wrapped', @@ -94,6 +99,7 @@ const TOKEN_REGISTRY = { decimals: 18, hasDeposit: true, nativeVersion: 'ETH', + zapEnabled: true, }, USDC: { type: 'erc20', @@ -101,6 +107,7 @@ const TOKEN_REGISTRY = { name: 'USD Coin', address: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', decimals: 6, + zapEnabled: true, }, USDT: { type: 'erc20', @@ -108,6 +115,7 @@ const TOKEN_REGISTRY = { name: 'Tether USD', address: '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', decimals: 6, + zapEnabled: true, }, }, @@ -119,6 +127,7 @@ const TOKEN_REGISTRY = { name: 'Ethereum', decimals: 18, wrappedVersion: 'WETH', + zapEnabled: true, }, WETH: { type: 'wrapped', @@ -128,6 +137,7 @@ const TOKEN_REGISTRY = { decimals: 18, hasDeposit: true, nativeVersion: 'ETH', + zapEnabled: true, }, USDC: { type: 'erc20', @@ -135,74 +145,7 @@ const TOKEN_REGISTRY = { name: 'USD Coin', address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', decimals: 6, - }, - }, - - // Polygon (Chain ID: 137) - 137: { - MATIC: { - type: 'native', - symbol: 'MATIC', - name: 'Polygon', - decimals: 18, - wrappedVersion: 'WMATIC', - }, - WMATIC: { - type: 'wrapped', - symbol: 'WMATIC', - name: 'Wrapped Matic', - address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - decimals: 18, - hasDeposit: true, - nativeVersion: 'MATIC', - }, - USDC: { - type: 'erc20', - symbol: 'USDC', - name: 'USD Coin', - address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', - decimals: 6, - }, - WETH: { - type: 'erc20', - symbol: 'WETH', - name: 'Wrapped Ethereum', - address: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619', - decimals: 18, - }, - }, - - // BSC (Chain ID: 56) - 56: { - BNB: { - type: 'native', - symbol: 'BNB', - name: 'BNB', - decimals: 18, - wrappedVersion: 'WBNB', - }, - WBNB: { - type: 'wrapped', - symbol: 'WBNB', - name: 'Wrapped BNB', - address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', - decimals: 18, - hasDeposit: true, - nativeVersion: 'BNB', - }, - USDC: { - type: 'erc20', - symbol: 'USDC', - name: 'USD Coin', - address: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', - decimals: 18, - }, - USDT: { - type: 'erc20', - symbol: 'USDT', - name: 'Tether USD', - address: '0x55d398326f99059fF775485246999027B3197955', - decimals: 18, + zapEnabled: true, }, }, }; @@ -214,8 +157,6 @@ const CHAIN_METADATA = { 1: { name: 'Ethereum', nativeToken: 'ETH' }, 42161: { name: 'Arbitrum', nativeToken: 'ETH' }, 8453: { name: 'Base', nativeToken: 'ETH' }, - 137: { name: 'Polygon', nativeToken: 'MATIC' }, - 56: { name: 'BSC', nativeToken: 'BNB' }, }; /** @@ -327,6 +268,41 @@ class TokenConfigService { return this.getToken(chainId, nativeToken.wrappedVersion); } + + /** + * Get zap-enabled tokens for a specific chain + * @param {number} chainId - Chain ID + * @returns {Object} - Formatted response with chain info and zap-enabled tokens + */ + static getZapTokens(chainId) { + const chainTokens = TOKEN_REGISTRY[chainId]; + const chainMetadata = CHAIN_METADATA[chainId]; + + if (!chainTokens || !chainMetadata) { + return null; + } + + // Filter only zap-enabled tokens and format for API response + const zapTokens = Object.values(chainTokens) + .filter(token => token.zapEnabled) + .map(token => ({ + symbol: token.symbol, + name: token.name, + address: token.address || 'native', + decimals: token.decimals, + type: token.type, + ...(token.wrappedVersion && { wrappedVersion: token.wrappedVersion }), + ...(token.nativeVersion && { nativeVersion: token.nativeVersion }), + ...(token.hasDeposit && { hasDeposit: token.hasDeposit }), + })); + + return { + chainId, + chainName: chainMetadata.name.toLowerCase(), + nativeToken: chainMetadata.nativeToken, + tokens: zapTokens, + }; + } } module.exports = { diff --git a/src/routes/tokens.js b/src/routes/tokens.js new file mode 100644 index 0000000..1b1152c --- /dev/null +++ b/src/routes/tokens.js @@ -0,0 +1,202 @@ +const express = require('express'); +const { TokenConfigService } = require('../config/tokenConfig'); + +const router = express.Router(); + +/** + * @swagger + * /tokens/zap/{chainId}: + * get: + * tags: + * - Tokens + * summary: Get zap-enabled tokens for a chain + * description: Returns all tokens available for zap operations on the specified blockchain network. Provides static metadata including contract addresses, decimals, and token types. + * parameters: + * - in: path + * name: chainId + * required: true + * schema: + * type: integer + * example: 1 + * description: Blockchain network ID (1=Ethereum, 42161=Arbitrum, 8453=Base) + * responses: + * 200: + * description: Zap-enabled tokens retrieved successfully + * content: + * application/json: + * schema: + * type: object + * required: [chainId, chainName, nativeToken, tokens] + * properties: + * chainId: + * type: integer + * example: 1 + * chainName: + * type: string + * example: "ethereum" + * nativeToken: + * type: string + * example: "ETH" + * tokens: + * type: array + * items: + * type: object + * required: [symbol, name, address, decimals, type] + * properties: + * symbol: + * type: string + * example: "USDC" + * name: + * type: string + * example: "USD Coin" + * address: + * type: string + * example: "0xA0b86a33E6441a8C8C5d56Aa14E4e66E8e6B9E2" + * decimals: + * type: integer + * example: 6 + * type: + * type: string + * enum: [native, wrapped, erc20] + * example: "erc20" + * wrappedVersion: + * type: string + * example: "WETH" + * nativeVersion: + * type: string + * example: "ETH" + * hasDeposit: + * type: boolean + * example: true + * examples: + * ethereumTokens: + * summary: Ethereum mainnet zap tokens + * value: + * chainId: 1 + * chainName: "ethereum" + * nativeToken: "ETH" + * tokens: + * - symbol: "ETH" + * name: "Ethereum" + * address: "native" + * decimals: 18 + * type: "native" + * wrappedVersion: "WETH" + * - symbol: "USDC" + * name: "USD Coin" + * address: "0xA0b86a33E6441a8C8C5d56Aa14E4e66E8e6B9E2" + * decimals: 6 + * type: "erc20" + * - symbol: "WETH" + * name: "Wrapped Ethereum" + * address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + * decimals: 18 + * type: "wrapped" + * nativeVersion: "ETH" + * hasDeposit: true + * 400: + * description: Invalid chain ID + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Invalid chain ID" + * message: + * type: string + * example: "Chain ID must be a valid integer" + * 404: + * description: Chain not supported + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * example: "Chain not supported" + * message: + * type: string + * example: "Chain ID 999 is not supported" + * supportedChains: + * type: array + * items: + * type: integer + * example: [1, 42161, 8453] + */ +router.get('/zap/:chainId', (req, res) => { + try { + const { chainId } = req.params; + + // Validate chain ID is a number + const parsedChainId = parseInt(chainId); + if (isNaN(parsedChainId)) { + return res.status(400).json({ + error: 'Invalid chain ID', + message: 'Chain ID must be a valid integer', + }); + } + + // Get zap tokens for the chain + const zapTokens = TokenConfigService.getZapTokens(parsedChainId); + + if (!zapTokens) { + return res.status(404).json({ + error: 'Chain not supported', + message: `Chain ID ${parsedChainId} is not supported`, + supportedChains: TokenConfigService.getSupportedChains(), + }); + } + + res.json(zapTokens); + } catch (_error) { + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to retrieve zap tokens', + }); + } +}); + +/** + * @swagger + * /tokens/chains: + * get: + * tags: + * - Tokens + * summary: Get supported blockchain networks + * description: Returns a list of all supported blockchain network IDs where zap operations are available + * responses: + * 200: + * description: Supported chains retrieved successfully + * content: + * application/json: + * schema: + * type: object + * required: [chains] + * properties: + * chains: + * type: array + * items: + * type: integer + * example: [1, 42161, 8453] + * examples: + * supportedChains: + * summary: Currently supported chains + * value: + * chains: [1, 42161, 8453] + */ +router.get('/chains', (req, res) => { + try { + const supportedChains = TokenConfigService.getSupportedChains(); + res.json({ chains: supportedChains }); + } catch (_error) { + res.status(500).json({ + error: 'Internal server error', + message: 'Failed to retrieve supported chains', + }); + } +}); + +module.exports = router; diff --git a/test/tokenConfig.test.js b/test/tokenConfig.test.js index e2e5db8..052b262 100644 --- a/test/tokenConfig.test.js +++ b/test/tokenConfig.test.js @@ -24,19 +24,23 @@ describe('TokenConfig', () => { it('should export TOKEN_REGISTRY with supported chains', () => { expect(TOKEN_REGISTRY).toBeDefined(); - expect(TOKEN_REGISTRY[1]).toBeDefined(); // Ethereum - expect(TOKEN_REGISTRY[42161]).toBeDefined(); // Arbitrum - expect(TOKEN_REGISTRY[8453]).toBeDefined(); // Base - expect(TOKEN_REGISTRY[137]).toBeDefined(); // Polygon - expect(TOKEN_REGISTRY[56]).toBeDefined(); // BSC + + const supportedChains = TokenConfigService.getSupportedChains(); + + supportedChains.forEach(chainId => { + expect(TOKEN_REGISTRY[chainId]).toBeDefined(); + expect(CHAIN_METADATA[chainId]).toBeDefined(); + }); + + expect(new Set(supportedChains)).toEqual( + new Set(Object.keys(TOKEN_REGISTRY).map(Number)) + ); }); it('should export CHAIN_METADATA', () => { expect(CHAIN_METADATA).toBeDefined(); expect(CHAIN_METADATA[1].name).toBe('Ethereum'); expect(CHAIN_METADATA[1].nativeToken).toBe('ETH'); - expect(CHAIN_METADATA[137].nativeToken).toBe('MATIC'); - expect(CHAIN_METADATA[56].nativeToken).toBe('BNB'); }); }); @@ -93,16 +97,6 @@ describe('TokenConfig', () => { expect(address).toBe('0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'); }); - it('should return WMATIC address for Polygon', () => { - const address = TokenConfigService.getWETHAddress(137); - expect(address).toBe('0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270'); - }); - - it('should return WBNB address for BSC', () => { - const address = TokenConfigService.getWETHAddress(56); - expect(address).toBe('0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'); - }); - it('should return null for unsupported chain', () => { const address = TokenConfigService.getWETHAddress(999999); expect(address).toBeNull(); @@ -114,8 +108,6 @@ describe('TokenConfig', () => { expect(TokenConfigService.getNativeTokenSymbol(1)).toBe('ETH'); expect(TokenConfigService.getNativeTokenSymbol(42161)).toBe('ETH'); expect(TokenConfigService.getNativeTokenSymbol(8453)).toBe('ETH'); - expect(TokenConfigService.getNativeTokenSymbol(137)).toBe('MATIC'); - expect(TokenConfigService.getNativeTokenSymbol(56)).toBe('BNB'); }); it('should return null for unsupported chain', () => { @@ -126,8 +118,6 @@ describe('TokenConfig', () => { describe('hasDepositFunction', () => { it('should return true for wrapped native tokens', () => { expect(TokenConfigService.hasDepositFunction(1, 'WETH')).toBe(true); - expect(TokenConfigService.hasDepositFunction(137, 'WMATIC')).toBe(true); - expect(TokenConfigService.hasDepositFunction(56, 'WBNB')).toBe(true); }); it('should return false for non-wrapped tokens', () => { @@ -166,8 +156,6 @@ describe('TokenConfig', () => { expect(chains).toContain(1); expect(chains).toContain(42161); expect(chains).toContain(8453); - expect(chains).toContain(137); - expect(chains).toContain(56); }); it('should return numbers, not strings', () => { @@ -183,8 +171,6 @@ describe('TokenConfig', () => { expect(TokenConfigService.isChainSupported(1)).toBe(true); expect(TokenConfigService.isChainSupported(42161)).toBe(true); expect(TokenConfigService.isChainSupported(8453)).toBe(true); - expect(TokenConfigService.isChainSupported(137)).toBe(true); - expect(TokenConfigService.isChainSupported(56)).toBe(true); }); it('should return false for unsupported chains', () => { @@ -199,14 +185,6 @@ describe('TokenConfig', () => { expect(weth).toBeDefined(); expect(weth.symbol).toBe('WETH'); expect(weth.type).toBe('wrapped'); - - const wmatic = TokenConfigService.getWrappedNativeToken(137); - expect(wmatic).toBeDefined(); - expect(wmatic.symbol).toBe('WMATIC'); - - const wbnb = TokenConfigService.getWrappedNativeToken(56); - expect(wbnb).toBeDefined(); - expect(wbnb.symbol).toBe('WBNB'); }); it('should return null for unsupported chain', () => {