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 a06c563e91..c249b07f5f 100644 --- a/packages/rest-api/package.json +++ b/packages/rest-api/package.json @@ -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" }, "description": "A node.js project exposing a rest api for synapse sdk quotes", diff --git a/packages/rest-api/src/controllers/bridgeController.ts b/packages/rest-api/src/controllers/bridgeController.ts index f5cffdb45c..7bb703631b 100644 --- a/packages/rest-api/src/controllers/bridgeController.ts +++ b/packages/rest-api/src/controllers/bridgeController.ts @@ -1,6 +1,5 @@ import { validationResult } from 'express-validator' import { parseUnits } from '@ethersproject/units' -import { isAddress } from 'ethers/lib/utils' import { formatBNToString } from '../utils/formatBNToString' import { Synapse } from '../services/synapseService' @@ -14,19 +13,9 @@ export const bridgeController = async (req, res) => { try { const { fromChain, toChain, amount, fromToken, toToken } = req.query - if (!isAddress(fromToken) || !isAddress(toToken)) { - return res.status(400).json({ error: 'Invalid token address' }) - } - const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken) const toTokenInfo = tokenAddressToToken(toChain.toString(), toToken) - if (!fromTokenInfo || !toTokenInfo) { - return res - .status(400) - .json({ error: 'Token not supported on specified chain' }) - } - const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const resp = await Synapse.allBridgeQuotes( diff --git a/packages/rest-api/src/controllers/bridgeTxInfoController.ts b/packages/rest-api/src/controllers/bridgeTxInfoController.ts index 43ac7910da..0a0b4bc7bc 100644 --- a/packages/rest-api/src/controllers/bridgeTxInfoController.ts +++ b/packages/rest-api/src/controllers/bridgeTxInfoController.ts @@ -1,6 +1,5 @@ import { validationResult } from 'express-validator' import { parseUnits } from '@ethersproject/units' -import { isAddress } from 'ethers/lib/utils' import { Synapse } from '../services/synapseService' import { tokenAddressToToken } from '../utils/tokenAddressToToken' @@ -15,18 +14,7 @@ export const bridgeTxInfoController = async (req, res) => { const { fromChain, toChain, amount, destAddress, fromToken, toToken } = req.query - if (!isAddress(fromToken) || !isAddress(toToken)) { - return res.status(400).json({ error: 'Invalid token address' }) - } - const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken) - const toTokenInfo = tokenAddressToToken(toChain.toString(), toToken) - - if (!fromTokenInfo || !toTokenInfo) { - return res - .status(400) - .json({ error: 'Token not supported on specified chain' }) - } const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) diff --git a/packages/rest-api/src/controllers/swapController.ts b/packages/rest-api/src/controllers/swapController.ts index 44716cef13..ac695af971 100644 --- a/packages/rest-api/src/controllers/swapController.ts +++ b/packages/rest-api/src/controllers/swapController.ts @@ -1,6 +1,6 @@ import { validationResult } from 'express-validator' import { formatUnits, parseUnits } from '@ethersproject/units' -import { isAddress } from 'ethers/lib/utils' +import { BigNumber } from '@ethersproject/bignumber' import { Synapse } from '../services/synapseService' import { tokenAddressToToken } from '../utils/tokenAddressToToken' @@ -13,19 +13,9 @@ export const swapController = async (req, res) => { try { const { chain, amount, fromToken, toToken } = req.query - if (!isAddress(fromToken) || !isAddress(toToken)) { - return res.status(400).json({ error: 'Invalid token address' }) - } - const fromTokenInfo = tokenAddressToToken(chain.toString(), fromToken) const toTokenInfo = tokenAddressToToken(chain.toString(), toToken) - if (!fromTokenInfo || !toTokenInfo) { - return res - .status(400) - .json({ error: 'Token not supported on specified chain' }) - } - const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) const quote = await Synapse.swapQuote( Number(chain), @@ -33,9 +23,15 @@ export const swapController = async (req, res) => { 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 e720219497..49f63891db 100644 --- a/packages/rest-api/src/controllers/swapTxInfoController.ts +++ b/packages/rest-api/src/controllers/swapTxInfoController.ts @@ -1,6 +1,5 @@ import { validationResult } from 'express-validator' import { parseUnits } from '@ethersproject/units' -import { isAddress } from 'ethers/lib/utils' import { Synapse } from '../services/synapseService' import { tokenAddressToToken } from '../utils/tokenAddressToToken' @@ -14,18 +13,7 @@ export const swapTxInfoController = async (req, res) => { try { const { chain, amount, address, fromToken, toToken } = req.query - if (!isAddress(fromToken) || !isAddress(toToken)) { - return res.status(400).json({ error: 'Invalid token address' }) - } - const fromTokenInfo = tokenAddressToToken(chain.toString(), fromToken) - const toTokenInfo = tokenAddressToToken(chain.toString(), toToken) - - if (!fromTokenInfo || !toTokenInfo) { - return res - .status(400) - .json({ error: 'Token not supported on specified chain' }) - } const amountInWei = parseUnits(amount.toString(), fromTokenInfo.decimals) diff --git a/packages/rest-api/src/routes/bridgeRoute.ts b/packages/rest-api/src/routes/bridgeRoute.ts index c4e03ac540..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 { isAddress } from 'ethers/lib/utils' +import { isTokenAddress } from '../utils/isTokenAddress' import { CHAINS_ARRAY } from '../constants/chains' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { bridgeController } from '../controllers/bridgeController' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -26,13 +27,21 @@ router.get( check('fromToken') .exists() .withMessage('fromToken is required') - .custom((value) => isAddress(value)) - .withMessage('Invalid fromToken address'), + .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) => isAddress(value)) - .withMessage('Invalid toToken address'), + .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, diff --git a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts index 73f2f74d45..8287143cea 100644 --- a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts +++ b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts @@ -5,6 +5,8 @@ import { isAddress } from 'ethers/lib/utils' import { CHAINS_ARRAY } from '../constants/chains' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { bridgeTxInfoController } from '../controllers/bridgeTxInfoController' +import { isTokenAddress } from '../utils/isTokenAddress' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -26,13 +28,21 @@ router.get( check('fromToken') .exists() .withMessage('fromToken is required') - .custom((value) => isAddress(value)) - .withMessage('Invalid fromToken address'), + .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) => isAddress(value)) - .withMessage('Invalid toToken address'), + .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() diff --git a/packages/rest-api/src/routes/swapRoute.ts b/packages/rest-api/src/routes/swapRoute.ts index 77d279fbc0..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 { isAddress } from 'ethers/lib/utils' 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() @@ -20,13 +21,21 @@ router.get( check('fromToken') .exists() .withMessage('fromToken is required') - .custom((value) => isAddress(value)) - .withMessage('Invalid fromToken address'), + .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) => isAddress(value)) - .withMessage('Invalid toToken address'), + .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 956d287aff..a54ce1cef9 100644 --- a/packages/rest-api/src/routes/swapTxInfoRoute.ts +++ b/packages/rest-api/src/routes/swapTxInfoRoute.ts @@ -5,6 +5,8 @@ import { isAddress } from 'ethers/lib/utils' import { CHAINS_ARRAY } from '../constants/chains' import { showFirstValidationError } from '../middleware/showFirstValidationError' import { swapTxInfoController } from '../controllers/swapTxInfoController' +import { isTokenAddress } from '../utils/isTokenAddress' +import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain' const router = express.Router() @@ -20,13 +22,21 @@ router.get( check('fromToken') .exists() .withMessage('fromToken is required') - .custom((value) => isAddress(value)) - .withMessage('Invalid fromToken address'), + .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) => isAddress(value)) - .withMessage('Invalid toToken address'), + .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() diff --git a/packages/rest-api/src/tests/bridgeRoute.test.ts b/packages/rest-api/src/tests/bridgeRoute.test.ts index e7795f69a4..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', + toChain: '10', fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum - toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC.e on Polygon + toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism amount: '1000', }) expect(response.status).toBe(200) @@ -42,7 +42,7 @@ describe('Bridge Route with Real Synapse Service', () => { fromChain: '1', toChain: '999', fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', amount: '1000', }) expect(response.status).toBe(400) @@ -64,12 +64,27 @@ describe('Bridge Route with Real Synapse Service', () => { ) }, 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: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + 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') diff --git a/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts b/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts index 41b9c77b8e..35ff81f19a 100644 --- a/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts +++ b/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts @@ -59,6 +59,22 @@ describe('Bridge TX Info Route', () => { ) }, 10_000) + it('should return 400 for token not supported on specified chain', async () => { + const response = await request(app).get('/bridgeTxInfo').query({ + fromChain: '1', + toChain: '137', + fromToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) + toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', + amount: '1000', + destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e', + }) + expect(response.status).toBe(400) + 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', diff --git a/packages/rest-api/src/tests/swapRoute.test.ts b/packages/rest-api/src/tests/swapRoute.test.ts index 4cf644e8d6..6c6d7ac43b 100644 --- a/packages/rest-api/src/tests/swapRoute.test.ts +++ b/packages/rest-api/src/tests/swapRoute.test.ts @@ -48,6 +48,21 @@ describe('Swap Route with Real Synapse Service', () => { ) }, 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', diff --git a/packages/rest-api/src/tests/swapTxInfoRoute.test.ts b/packages/rest-api/src/tests/swapTxInfoRoute.test.ts index 401857d376..3faf9ca8ea 100644 --- a/packages/rest-api/src/tests/swapTxInfoRoute.test.ts +++ b/packages/rest-api/src/tests/swapTxInfoRoute.test.ts @@ -63,6 +63,21 @@ describe('Swap TX Info Route with Real Synapse Service', () => { ) }, 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: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + toToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported) + 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 missing amount, with error message', async () => { const response = await request(app).get('/swapTxInfo').query({ chain: '1', 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 + ) +}