diff --git a/packages/rest-api/jest.config.js b/packages/rest-api/jest.config.js index 23f6f815f5..ba447263ea 100644 --- a/packages/rest-api/jest.config.js +++ b/packages/rest-api/jest.config.js @@ -6,5 +6,5 @@ module.exports = { '^.+\\.(ts|tsx)$': 'babel-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - moduleDirectories: ['node_modules', 'src'], + moduleDirectories: ['node_modules', ''], } diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json index 2dad80fe96..f00049a91d 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -15,7 +15,7 @@ "lint:check": "eslint . --max-warnings=0", "ci:lint": "npm run lint:check", "test": "jest", - "test:coverage": "echo 'No tests defined.'" + "test:coverage": "jest --collect-coverage" }, "dependencies": { "@ethersproject/bignumber": "^5.7.0", @@ -26,6 +26,7 @@ "ethers": "5.7.2", "express": "^4.18.2", "express-validator": "^7.2.0", + "jest": "^29.7.0", "lodash": "^4.17.21", "supertest": "^6.3.3", "typescript": "^4.8.3" diff --git a/packages/rest-api/src/controllers/bridgeController.ts b/packages/rest-api/src/controllers/bridgeController.ts index aeead94c52..7bb703631b 100644 --- a/packages/rest-api/src/controllers/bridgeController.ts +++ b/packages/rest-api/src/controllers/bridgeController.ts @@ -3,6 +3,7 @@ import { parseUnits } from '@ethersproject/units' import { formatBNToString } from '../utils/formatBNToString' import { Synapse } from '../services/synapseService' +import { tokenAddressToToken } from '../utils/tokenAddressToToken' export const bridgeController = async (req, res) => { const errors = validationResult(req) @@ -10,17 +11,18 @@ export const bridgeController = async (req, res) => { return res.status(400).json({ errors: errors.array() }) } try { - const { fromChain, toChain, amount } = req.query - const fromTokenInfo = res.locals.tokenInfo.fromToken - const toTokenInfo = res.locals.tokenInfo.toToken + const { fromChain, toChain, amount, fromToken, toToken } = req.query + + const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken) + const toTokenInfo = tokenAddressToToken(toChain.toString(), toToken) const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const resp = await Synapse.allBridgeQuotes( Number(fromChain), Number(toChain), - fromTokenInfo.address, - toTokenInfo.address, + fromToken, + toToken, amountInWei ) const payload = resp.map((quote) => ({ diff --git a/packages/rest-api/src/controllers/bridgeTxInfoController.ts b/packages/rest-api/src/controllers/bridgeTxInfoController.ts index 3f75330dee..0a0b4bc7bc 100644 --- a/packages/rest-api/src/controllers/bridgeTxInfoController.ts +++ b/packages/rest-api/src/controllers/bridgeTxInfoController.ts @@ -2,6 +2,7 @@ import { validationResult } from 'express-validator' import { parseUnits } from '@ethersproject/units' import { Synapse } from '../services/synapseService' +import { tokenAddressToToken } from '../utils/tokenAddressToToken' export const bridgeTxInfoController = async (req, res) => { const errors = validationResult(req) @@ -10,17 +11,18 @@ export const bridgeTxInfoController = async (req, res) => { } try { - const { fromChain, toChain, amount, destAddress } = req.query - const fromTokenInfo = res.locals.tokenInfo.fromToken - const toTokenInfo = res.locals.tokenInfo.toToken + const { fromChain, toChain, amount, destAddress, fromToken, toToken } = + req.query + + const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken) const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const quotes = await Synapse.allBridgeQuotes( Number(fromChain), Number(toChain), - fromTokenInfo.address, - toTokenInfo.address, + fromToken, + toToken, amountInWei ) @@ -31,7 +33,7 @@ export const bridgeTxInfoController = async (req, res) => { quote.routerAddress, Number(fromChain), Number(toChain), - fromTokenInfo.address, + fromToken, amountInWei, quote.originQuery, quote.destQuery diff --git a/packages/rest-api/src/controllers/swapController.ts b/packages/rest-api/src/controllers/swapController.ts index 53d28412bb..ac695af971 100644 --- a/packages/rest-api/src/controllers/swapController.ts +++ b/packages/rest-api/src/controllers/swapController.ts @@ -1,7 +1,9 @@ import { validationResult } from 'express-validator' import { formatUnits, parseUnits } from '@ethersproject/units' +import { BigNumber } from '@ethersproject/bignumber' import { Synapse } from '../services/synapseService' +import { tokenAddressToToken } from '../utils/tokenAddressToToken' export const swapController = async (req, res) => { const errors = validationResult(req) @@ -9,20 +11,27 @@ export const swapController = async (req, res) => { return res.status(400).json({ errors: errors.array() }) } try { - const { chain, amount } = req.query - const fromTokenInfo = res.locals.tokenInfo.fromToken - const toTokenInfo = res.locals.tokenInfo.toToken + const { chain, amount, fromToken, toToken } = req.query + + const fromTokenInfo = tokenAddressToToken(chain.toString(), fromToken) + const toTokenInfo = tokenAddressToToken(chain.toString(), toToken) const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const quote = await Synapse.swapQuote( Number(chain), - fromTokenInfo.address, - toTokenInfo.address, + fromToken, + toToken, amountInWei ) + + const formattedMaxAmountOut = formatUnits( + BigNumber.from(quote.maxAmountOut), + toTokenInfo.decimals + ) + res.json({ - maxAmountOut: formatUnits(quote.maxAmountOut, toTokenInfo.decimals), ...quote, + maxAmountOut: formattedMaxAmountOut, }) } catch (err) { res.status(500).json({ diff --git a/packages/rest-api/src/controllers/swapTxInfoController.ts b/packages/rest-api/src/controllers/swapTxInfoController.ts index 9577d3ceea..49f63891db 100644 --- a/packages/rest-api/src/controllers/swapTxInfoController.ts +++ b/packages/rest-api/src/controllers/swapTxInfoController.ts @@ -2,6 +2,7 @@ import { validationResult } from 'express-validator' import { parseUnits } from '@ethersproject/units' import { Synapse } from '../services/synapseService' +import { tokenAddressToToken } from '../utils/tokenAddressToToken' export const swapTxInfoController = async (req, res) => { const errors = validationResult(req) @@ -10,23 +11,23 @@ export const swapTxInfoController = async (req, res) => { } try { - const { chain, amount } = req.query - const fromTokenInfo = res.locals.tokenInfo.fromToken - const toTokenInfo = res.locals.tokenInfo.toToken + const { chain, amount, address, fromToken, toToken } = req.query + + const fromTokenInfo = tokenAddressToToken(chain.toString(), fromToken) const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const quote = await Synapse.swapQuote( Number(chain), - fromTokenInfo.address, - toTokenInfo.address, + fromToken, + toToken, amountInWei ) const txInfo = await Synapse.swap( Number(chain), - fromTokenInfo.address, - toTokenInfo.address, + address, + fromToken, amountInWei, quote.query ) diff --git a/packages/rest-api/src/routes/bridgeRoute.ts b/packages/rest-api/src/routes/bridgeRoute.ts index cb00696b5b..b27e9c99d8 100644 --- a/packages/rest-api/src/routes/bridgeRoute.ts +++ b/packages/rest-api/src/routes/bridgeRoute.ts @@ -1,10 +1,11 @@ import express from 'express' import { check } from 'express-validator' +import { isTokenAddress } from '../utils/isTokenAddress' import { CHAINS_ARRAY } from '../constants/chains' -import { validateTokens } from '../validations/validateTokens' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { bridgeController } from '../controllers/bridgeController' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -23,9 +24,25 @@ router.get( .withMessage('Unsupported toChain') .exists() .withMessage('toChain is required'), - validateTokens('fromChain', 'fromToken', 'fromToken'), - validateTokens('toChain', 'toToken', 'toToken'), - check('amount').isNumeric(), + check('fromToken') + .exists() + .withMessage('fromToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid fromToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.fromChain as string) + ) + .withMessage('Token not supported on specified chain'), + check('toToken') + .exists() + .withMessage('toToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid toToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.toChain as string) + ) + .withMessage('Token not supported on specified chain'), + check('amount').isNumeric().exists().withMessage('amount is required'), ], showFirstValidationError, bridgeController diff --git a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts index 8929f66202..8287143cea 100644 --- a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts +++ b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts @@ -1,10 +1,12 @@ import express from 'express' import { check } from 'express-validator' +import { isAddress } from 'ethers/lib/utils' import { CHAINS_ARRAY } from '../constants/chains' -import { validateTokens } from '../validations/validateTokens' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { bridgeTxInfoController } from '../controllers/bridgeTxInfoController' +import { isTokenAddress } from '../utils/isTokenAddress' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -23,10 +25,30 @@ router.get( .withMessage('Unsupported toChain') .exists() .withMessage('toChain is required'), - validateTokens('fromChain', 'fromToken', 'fromToken'), - validateTokens('toChain', 'toToken', 'toToken'), - check('amount').isNumeric(), - check('destAddress').isString(), + check('fromToken') + .exists() + .withMessage('fromToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid fromToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.fromChain as string) + ) + .withMessage('Token not supported on specified chain'), + check('toToken') + .exists() + .withMessage('toToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid toToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.toChain as string) + ) + .withMessage('Token not supported on specified chain'), + check('amount').isNumeric().exists().withMessage('amount is required'), + check('destAddress') + .exists() + .withMessage('destAddress is required') + .custom((value) => isAddress(value)) + .withMessage('Invalid destination address'), ], showFirstValidationError, bridgeTxInfoController diff --git a/packages/rest-api/src/routes/swapRoute.ts b/packages/rest-api/src/routes/swapRoute.ts index 8aaeac316f..931efec5ef 100644 --- a/packages/rest-api/src/routes/swapRoute.ts +++ b/packages/rest-api/src/routes/swapRoute.ts @@ -1,10 +1,11 @@ import express from 'express' import { check } from 'express-validator' -import { validateTokens } from '../validations/validateTokens' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { swapController } from '../controllers/swapController' import { CHAINS_ARRAY } from '../constants/chains' +import { isTokenAddress } from '../utils/isTokenAddress' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -17,8 +18,24 @@ router.get( .withMessage('Unsupported chain') .exists() .withMessage('chain is required'), - validateTokens('chain', 'fromToken', 'fromToken'), - validateTokens('chain', 'toToken', 'toToken'), + check('fromToken') + .exists() + .withMessage('fromToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid fromToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.chain as string) + ) + .withMessage('Token not supported on specified chain'), + check('toToken') + .exists() + .withMessage('toToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid toToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.chain as string) + ) + .withMessage('Token not supported on specified chain'), check('amount').isNumeric().exists().withMessage('amount is required'), ], showFirstValidationError, diff --git a/packages/rest-api/src/routes/swapTxInfoRoute.ts b/packages/rest-api/src/routes/swapTxInfoRoute.ts index 9cc9805b6d..a54ce1cef9 100644 --- a/packages/rest-api/src/routes/swapTxInfoRoute.ts +++ b/packages/rest-api/src/routes/swapTxInfoRoute.ts @@ -1,10 +1,12 @@ import express from 'express' import { check } from 'express-validator' +import { isAddress } from 'ethers/lib/utils' import { CHAINS_ARRAY } from '../constants/chains' -import { validateTokens } from '../validations/validateTokens' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { swapTxInfoController } from '../controllers/swapTxInfoController' +import { isTokenAddress } from '../utils/isTokenAddress' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -17,9 +19,30 @@ router.get( .withMessage('Unsupported chain') .exists() .withMessage('chain is required'), - validateTokens('chain', 'fromToken', 'fromToken'), - validateTokens('chain', 'toToken', 'toToken'), - check('amount').isNumeric(), + check('fromToken') + .exists() + .withMessage('fromToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid fromToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.chain as string) + ) + .withMessage('Token not supported on specified chain'), + check('toToken') + .exists() + .withMessage('toToken is required') + .custom((value) => isTokenAddress(value)) + .withMessage('Invalid toToken address') + .custom((value, { req }) => + isTokenSupportedOnChain(value, req.query.chain as string) + ) + .withMessage('Token not supported on specified chain'), + check('amount').isNumeric().exists().withMessage('amount is required'), + check('address') + .exists() + .withMessage('address is required') + .custom((value) => isAddress(value)) + .withMessage('Invalid Ethereum address'), ], showFirstValidationError, swapTxInfoController diff --git a/packages/rest-api/src/tests/bridgeRoute.test.ts b/packages/rest-api/src/tests/bridgeRoute.test.ts index 16addfc80f..dcef8451ec 100644 --- a/packages/rest-api/src/tests/bridgeRoute.test.ts +++ b/packages/rest-api/src/tests/bridgeRoute.test.ts @@ -7,12 +7,12 @@ const app = express() app.use('/bridge', bridgeRoute) describe('Bridge Route with Real Synapse Service', () => { - it('should return bridge quotes for valid input, 1000 USDC from Ethereum to Polygon', async () => { + it('should return bridge quotes for valid input, 1000 USDC from Ethereum to Optimism', async () => { const response = await request(app).get('/bridge').query({ fromChain: '1', - toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + toChain: '10', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum + toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism amount: '1000', }) expect(response.status).toBe(200) @@ -26,8 +26,8 @@ describe('Bridge Route with Real Synapse Service', () => { const response = await request(app).get('/bridge').query({ fromChain: '999', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', }) expect(response.status).toBe(400) @@ -41,34 +41,51 @@ describe('Bridge Route with Real Synapse Service', () => { const response = await request(app).get('/bridge').query({ fromChain: '1', toChain: '999', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', amount: '1000', }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('message', 'Unsupported toChain') }, 10000) - it('should return 400 for missing fromToken, with error message', async () => { + it('should return 400 for invalid fromToken address, with error message', async () => { const response = await request(app).get('/bridge').query({ fromChain: '1', toChain: '137', - toToken: 'USDC', + fromToken: 'invalid_address', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', }) - expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('field', 'fromToken') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid fromToken address' + ) }, 10000) - it('should return 400 for missing amount, with error message', async () => { + it('should return 400 for token not supported on specified chain, with error message', async () => { const response = await request(app).get('/bridge').query({ fromChain: '1', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + amount: '1000', }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid fromToken address' + ) + }, 10000) + it('should return 400 for missing amount, with error message', async () => { + const response = await request(app).get('/bridge').query({ + fromChain: '1', + toChain: '10', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('field', 'amount') }, 10000) diff --git a/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts b/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts index 8e83702336..35ff81f19a 100644 --- a/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts +++ b/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts @@ -11,8 +11,8 @@ describe('Bridge TX Info Route', () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '1', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon amount: '1000', destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) @@ -31,8 +31,8 @@ describe('Bridge TX Info Route', () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '999', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) @@ -43,52 +43,63 @@ describe('Bridge TX Info Route', () => { ) }, 10_000) - it('should return 400 for unsupported toChain', async () => { + it('should return 400 for invalid fromToken address', async () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '1', - toChain: '999', - fromToken: 'USDC', - toToken: 'USDC', + toChain: '137', + fromToken: 'invalid_address', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('message', 'Unsupported toChain') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid fromToken address' + ) }, 10_000) - it('should return 400 for missing fromToken', async () => { + it('should return 400 for token not supported on specified chain', async () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '1', toChain: '137', - toToken: 'USDC', + fromToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('field', 'fromToken') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid fromToken address' + ) }, 10_000) it('should return 400 for missing amount', async () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '1', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('field', 'amount') }, 10_000) - it('should return 400 for missing destAddress', async () => { + it('should return 400 for invalid destAddress', async () => { const response = await request(app).get('/bridgeTxInfo').query({ fromChain: '1', toChain: '137', - fromToken: 'USDC', - toToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', amount: '1000', + destAddress: 'invalid_address', }) expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('field', 'destAddress') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid destination address' + ) }, 10_000) }) diff --git a/packages/rest-api/src/tests/swapRoute.test.ts b/packages/rest-api/src/tests/swapRoute.test.ts index 24d2e05861..6c6d7ac43b 100644 --- a/packages/rest-api/src/tests/swapRoute.test.ts +++ b/packages/rest-api/src/tests/swapRoute.test.ts @@ -10,8 +10,8 @@ describe('Swap Route with Real Synapse Service', () => { it('should return a real swap quote for valid input, 1000 USDC', async () => { const response = await request(app).get('/swap').query({ chain: '1', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum amount: '1000', }) @@ -24,32 +24,50 @@ describe('Swap Route with Real Synapse Service', () => { it('should return 400 for unsupported chain, with error message', async () => { const response = await request(app).get('/swap').query({ chain: '111', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', amount: '1000', }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('message', 'Unsupported chain') - expect(response.body.error).toHaveProperty('field', 'chain') }, 10_000) - it('should return 400 for missing toToken, with error message', async () => { + it('should return 400 for invalid toToken address, with error message', async () => { const response = await request(app).get('/swap').query({ chain: '1', - fromToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: 'invalid_address', amount: '1000', }) expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('field', 'toToken') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid toToken address' + ) + }, 10_000) + + it('should return 400 for token not supported on specified chain', async () => { + const response = await request(app).get('/swap').query({ + chain: '1', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) + amount: '1000', + }) + + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid toToken address' + ) }, 10_000) it('should return 400 for missing amount, with error message', async () => { const response = await request(app).get('/swap').query({ chain: '1', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', }) expect(response.status).toBe(400) diff --git a/packages/rest-api/src/tests/swapTxInfoRoute.test.ts b/packages/rest-api/src/tests/swapTxInfoRoute.test.ts index 3c682006da..3faf9ca8ea 100644 --- a/packages/rest-api/src/tests/swapTxInfoRoute.test.ts +++ b/packages/rest-api/src/tests/swapTxInfoRoute.test.ts @@ -10,9 +10,10 @@ describe('Swap TX Info Route with Real Synapse Service', () => { it('should return transaction info for valid input, 1000 USDC to DAI', async () => { const response = await request(app).get('/swapTxInfo').query({ chain: '1', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum amount: '1000', + address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(200) expect(response.body).toHaveProperty('data') @@ -20,33 +21,69 @@ describe('Swap TX Info Route with Real Synapse Service', () => { expect(response.body).toHaveProperty('value') }, 10_000) + it('should return 400 for invalid address, with error message', async () => { + const response = await request(app).get('/swapTxInfo').query({ + chain: '1', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + amount: '1000', + address: 'invalid_address', + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid Ethereum address' + ) + }, 10_000) + it('should return 400 for unsupported chain, with error message', async () => { const response = await request(app).get('/swapTxInfo').query({ chain: '111', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', amount: '1000', + address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('message', 'Unsupported chain') - expect(response.body.error).toHaveProperty('field', 'chain') }, 10_000) - it('should return 400 for missing toToken, with error message', async () => { + it('should return 400 for invalid toToken address, with error message', async () => { + const response = await request(app).get('/swapTxInfo').query({ + chain: '1', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: 'invalid_address', + amount: '1000', + address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + }) + expect(response.status).toBe(400) + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid toToken address' + ) + }, 10_000) + + it('should return 400 for token not supported on specified chain', async () => { const response = await request(app).get('/swapTxInfo').query({ chain: '1', - fromToken: 'USDC', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) amount: '1000', + address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) - expect(response.body.error).toHaveProperty('field', 'toToken') + expect(response.body.error).toHaveProperty( + 'message', + 'Invalid toToken address' + ) }, 10_000) it('should return 400 for missing amount, with error message', async () => { const response = await request(app).get('/swapTxInfo').query({ chain: '1', - fromToken: 'USDC', - toToken: 'DAI', + fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', }) expect(response.status).toBe(400) expect(response.body.error).toHaveProperty('field', 'amount') diff --git a/packages/rest-api/src/utils/isTokenAddress.ts b/packages/rest-api/src/utils/isTokenAddress.ts new file mode 100644 index 0000000000..7945b1ba4c --- /dev/null +++ b/packages/rest-api/src/utils/isTokenAddress.ts @@ -0,0 +1,12 @@ +import { BridgeableToken } from '../types' +import * as bridgeableTokens from '../constants/bridgeable' + +export const isTokenAddress = (address: string): boolean => { + const normalizedAddress = address.toLowerCase() + + return Object.values(bridgeableTokens).some((token: BridgeableToken) => + Object.values(token.addresses).some( + (tokenAddress: string) => tokenAddress.toLowerCase() === normalizedAddress + ) + ) +} diff --git a/packages/rest-api/src/utils/isTokenSupportedOnChain.ts b/packages/rest-api/src/utils/isTokenSupportedOnChain.ts new file mode 100644 index 0000000000..161404a6f6 --- /dev/null +++ b/packages/rest-api/src/utils/isTokenSupportedOnChain.ts @@ -0,0 +1,16 @@ +import { BridgeableToken } from '../types' +import * as bridgeableTokens from '../constants/bridgeable' + +export const isTokenSupportedOnChain = ( + tokenAddress: string, + chainId: string +): boolean => { + const normalizedAddress = tokenAddress.toLowerCase() + const chainIdNumber = parseInt(chainId, 10) + + return Object.values(bridgeableTokens).some( + (token: BridgeableToken) => + token.addresses[chainIdNumber] !== undefined && + token.addresses[chainIdNumber].toLowerCase() === normalizedAddress + ) +} diff --git a/packages/rest-api/src/utils/tokenAddressToToken.ts b/packages/rest-api/src/utils/tokenAddressToToken.ts new file mode 100644 index 0000000000..f28c9b86e1 --- /dev/null +++ b/packages/rest-api/src/utils/tokenAddressToToken.ts @@ -0,0 +1,17 @@ +import { BRIDGE_MAP } from '../constants/bridgeMap' + +export const tokenAddressToToken = (chain: string, tokenAddress: string) => { + const chainData = BRIDGE_MAP[chain] + if (!chainData) { + return null + } + const tokenInfo = chainData[tokenAddress] + if (!tokenInfo) { + return null + } + return { + address: tokenAddress, + symbol: tokenInfo.symbol, + decimals: tokenInfo.decimals, + } +} diff --git a/packages/rest-api/src/utils/findTokenInfo.ts b/packages/rest-api/src/utils/tokenSymbolToToken.ts similarity index 82% rename from packages/rest-api/src/utils/findTokenInfo.ts rename to packages/rest-api/src/utils/tokenSymbolToToken.ts index 7cd4e9d63d..bed196c851 100644 --- a/packages/rest-api/src/utils/findTokenInfo.ts +++ b/packages/rest-api/src/utils/tokenSymbolToToken.ts @@ -1,6 +1,6 @@ import { BRIDGE_MAP } from '../constants/bridgeMap' -export const findTokenInfo = (chain: string, tokenSymbol: string) => { +export const tokenSymbolToToken = (chain: string, tokenSymbol: string) => { const chainData = BRIDGE_MAP[chain] if (!chainData) { return null diff --git a/packages/rest-api/src/validations/validateTokens.ts b/packages/rest-api/src/validations/validateTokens.ts index c787115e1e..6e89a9c70c 100644 --- a/packages/rest-api/src/validations/validateTokens.ts +++ b/packages/rest-api/src/validations/validateTokens.ts @@ -1,6 +1,6 @@ import { check } from 'express-validator' -import { findTokenInfo } from '../utils/findTokenInfo' +import { tokenSymbolToToken } from '../utils/tokenSymbolToToken' export const validateTokens = (chainParam, tokenParam, paramName) => { return check(tokenParam) @@ -9,7 +9,7 @@ export const validateTokens = (chainParam, tokenParam, paramName) => { .withMessage(`${paramName} is required`) .custom((value, { req }) => { const chain = req.query[chainParam] - const tokenInfo = findTokenInfo(chain, value) + const tokenInfo = tokenSymbolToToken(chain, value) if (!tokenInfo) { throw new Error(`Invalid ${paramName} symbol`) }