Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds /destinationTokens route [SLT-204] #3151

Merged
merged 7 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/rest-api/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ module.exports = {
files: ['jest.config.js'],
rules: {
'prettier/prettier': 'off',
'guard-for-in': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'guard-for-in': 'off',
abtestingalpha marked this conversation as resolved.
Show resolved Hide resolved
},
},
],
Expand Down
3 changes: 1 addition & 2 deletions packages/rest-api/src/constants/bridgeable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BridgeableToken } from '../types'
import { CHAINS } from './chains'

const ZeroAddress = '0x0000000000000000000000000000000000000000'
import { ZeroAddress } from '.'

export const GOHM: BridgeableToken = {
addresses: {
Expand Down
3 changes: 3 additions & 0 deletions packages/rest-api/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export const VALID_BRIDGE_MODULES = [
'SynapseCCTP',
'SynapseRFQ',
]

export const ZeroAddress = '0x0000000000000000000000000000000000000000'
export const EthAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { validationResult } from 'express-validator'

import { tokenAddressToToken } from '../utils/tokenAddressToToken'
import { BRIDGE_ROUTE_MAPPING } from '../utils/bridgeRouteMapping'

export const destinationTokensController = async (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}

try {
const { fromChain, fromToken } = req.query

const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken)

const constructedKey = `${fromTokenInfo.symbol}-${fromChain}`

const options = BRIDGE_ROUTE_MAPPING[constructedKey]

res.json(options)
} catch (err) {
res.status(500).json({
error:
'An unexpected error occurred in /destinationTokens. Please try again later.',
details: err.message,
})
}
}
38 changes: 38 additions & 0 deletions packages/rest-api/src/routes/destinationTokensRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import express from 'express'
import { check } from 'express-validator'
import { isAddress } from 'ethers/lib/utils'

import { CHAINS_ARRAY } from '../constants/chains'
import { showFirstValidationError } from '../middleware/showFirstValidationError'
import { destinationTokensController } from '../controllers/destinationTokensController'
import { isTokenAddress } from '../utils/isTokenAddress'
import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain'

const router = express.Router()

router.get(
'/',
[
check('fromChain')
.exists()
.withMessage('fromChain is required')
.isNumeric()
.custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value)))
.withMessage('Unsupported fromChain'),
check('fromToken')
.exists()
.withMessage('fromToken is required')
.custom((value) => isAddress(value))
.withMessage('Invalid fromToken address')
.custom((value) => isTokenAddress(value))
.withMessage('Unsupported fromToken address')
.custom((value, { req }) =>
isTokenSupportedOnChain(value, req.query.fromChain as string)
)
.withMessage('Token not supported on specified chain'),
],
showFirstValidationError,
destinationTokensController
)

export default router
2 changes: 2 additions & 0 deletions packages/rest-api/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import getSynapseTxIdRoute from './getSynapseTxIdRoute'
import getBridgeTxStatusRoute from './getBridgeTxStatusRoute'
import getDestinationTxRoute from './getDestinationTxRoute'
import tokenListRoute from './tokenListRoute'
import destinationTokensRoute from './destinationTokensRoute'

const router = express.Router()

Expand All @@ -21,5 +22,6 @@ router.use('/getSynapseTxId', getSynapseTxIdRoute)
router.use('/getBridgeTxStatus', getBridgeTxStatusRoute)
router.use('/getDestinationTx', getDestinationTxRoute)
router.use('/tokenList', tokenListRoute)
router.use('/destinationTokens', destinationTokensRoute)

export default router
99 changes: 99 additions & 0 deletions packages/rest-api/src/tests/destinationTokensRoute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import request from 'supertest'
import express from 'express'

import destinationTokensRoute from '../routes/destinationTokensRoute'

const app = express()
app.use('/destinationTokens', destinationTokensRoute)

describe('destinatonTokens Route', () => {
it('should return destination tokens for valid input', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
})

expect(response.status).toBe(200)
expect(Array.isArray(response.body)).toBe(true)
expect(response.body.length).toBeGreaterThan(0)
expect(response.body[0]).toHaveProperty('symbol')
expect(response.body[0]).toHaveProperty('address')
expect(response.body[0]).toHaveProperty('chainId')
})

it('should return 400 for unsupported fromChain', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '999',
fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'Unsupported fromChain'
)
})

it('should return 400 for invalid fromToken address', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
fromToken: 'invalid_address',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'Invalid fromToken address'
)
})

it('should return 400 for token not supported by Synapse', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
fromToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'Unsupported fromToken address'
)
})

it('should return 400 for token not supported on specified chain', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '10',
fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'Token not supported on specified chain'
)
})

it('should return 400 for missing fromChain', async () => {
const response = await request(app).get('/destinationTokens').query({
fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'fromChain is required'
)
})

it('should return 400 for missing fromToken', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
})

expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'fromToken is required'
)
})
})
67 changes: 67 additions & 0 deletions packages/rest-api/src/utils/bridgeRouteMapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { EthAddress, ZeroAddress } from '../constants'
import { BRIDGE_MAP } from '../constants/bridgeMap'

interface TokenInfo {
symbol: string
address: string
chainId: string
}

interface BridgeRoutes {
[key: string]: TokenInfo[]
}

const constructJSON = (
swappableMap: typeof BRIDGE_MAP,
exclusionList: string[]
): BridgeRoutes => {
const result: BridgeRoutes = {}

for (const chainA in swappableMap) {
for (const addressA in swappableMap[chainA]) {
const tokenA = swappableMap[chainA][addressA]
const keyA = `${tokenA.symbol}-${chainA}`

if (exclusionList.includes(keyA)) {
continue
}

result[keyA] = []

for (const chainB in swappableMap) {
if (chainA !== chainB) {
for (const addressB in swappableMap[chainB]) {
const tokenB = swappableMap[chainB][addressB]
const keyB = `${tokenB.symbol}-${chainB}`

if (exclusionList.includes(keyB)) {
continue
}

const canBridge = tokenA.origin.some(
(bridgeSymbol) =>
tokenB.destination.includes(bridgeSymbol) ||
tokenB.origin.includes(bridgeSymbol)
)

if (canBridge) {
result[keyA].push({
symbol: tokenB.symbol,
address: addressB === EthAddress ? ZeroAddress : addressB,
chainId: chainB,
})
}
}
}
}

if (result[keyA].length === 0) {
delete result[keyA]
}
}
}

return result
}

export const BRIDGE_ROUTE_MAPPING: BridgeRoutes = constructJSON(BRIDGE_MAP, [])
6 changes: 5 additions & 1 deletion packages/rest-api/src/utils/tokenAddressToToken.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { EthAddress, ZeroAddress } from '../constants'
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]

const address = tokenAddress === ZeroAddress ? EthAddress : tokenAddress

const tokenInfo = chainData[address]
if (!tokenInfo) {
return null
}
Expand Down
Loading