Skip to content

Commit

Permalink
feat(api): bridge limits [SLT-165] (#3179)
Browse files Browse the repository at this point in the history
* adds `/bridgeLimits` route, controller

* fetch best sdk quote for min/max origin amounts

* add tests

* implement middleware to normalize addresses

* adds swagger doc
  • Loading branch information
bigboydiamonds authored Sep 24, 2024
1 parent 038605d commit 98362bb
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test:coverage": "jest --collect-coverage"
},
"dependencies": {
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@ethersproject/units": "5.7.0",
Expand Down
108 changes: 108 additions & 0 deletions packages/rest-api/src/controllers/bridgeLimitsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { validationResult } from 'express-validator'
import { BigNumber } from 'ethers'
import { parseUnits } from '@ethersproject/units'

import { Synapse } from '../services/synapseService'
import { tokenAddressToToken } from '../utils/tokenAddressToToken'
import { formatBNToString } from '../utils/formatBNToString'

export const bridgeLimitsController = async (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
try {
const { fromChain, fromToken, toChain, toToken } = req.query

const fromTokenInfo = tokenAddressToToken(fromChain, fromToken)
const toTokenInfo = tokenAddressToToken(toChain, toToken)

const upperLimitValue = parseUnits('1000000', fromTokenInfo.decimals)
const upperLimitBridgeQuotes = await Synapse.allBridgeQuotes(
Number(fromChain),
Number(toChain),
fromTokenInfo.address,
toTokenInfo.address,
upperLimitValue
)

const lowerLimitValues = ['0.01', '10']
let lowerLimitBridgeQuotes = null

for (const limit of lowerLimitValues) {
const lowerLimitAmount = parseUnits(limit, fromTokenInfo.decimals)

lowerLimitBridgeQuotes = await Synapse.allBridgeQuotes(
Number(fromChain),
Number(toChain),
fromTokenInfo.address,
toTokenInfo.address,
lowerLimitAmount
)

if (lowerLimitBridgeQuotes && lowerLimitBridgeQuotes.length > 0) {
break
}
}

const maxBridgeAmountQuote = upperLimitBridgeQuotes.reduce(
(maxQuote, currentQuote) => {
const currentMaxAmount = currentQuote.maxAmountOut
const maxAmount = maxQuote ? maxQuote.maxAmountOut : BigNumber.from(0)

return currentMaxAmount.gt(maxAmount) ? currentQuote : maxQuote
},
null
)

const minBridgeAmountQuote = lowerLimitBridgeQuotes.reduce(
(minQuote, currentQuote) => {
const currentFeeAmount = currentQuote.feeAmount
const minFeeAmount = minQuote ? minQuote.feeAmount : null

return !minFeeAmount || currentFeeAmount.lt(minFeeAmount)
? currentQuote
: minQuote
},
null
)

if (!maxBridgeAmountQuote || !minBridgeAmountQuote) {
return res.json({
maxOriginAmount: null,
minOriginAmount: null,
})
}

const maxAmountOriginQueryTokenOutInfo = tokenAddressToToken(
toChain,
maxBridgeAmountQuote.destQuery.tokenOut
)

const minAmountOriginQueryTokenOutInfo = tokenAddressToToken(
fromChain,
minBridgeAmountQuote.originQuery.tokenOut
)

const maxOriginAmount = formatBNToString(
maxBridgeAmountQuote.maxAmountOut,
maxAmountOriginQueryTokenOutInfo.decimals
)

const minOriginAmount = formatBNToString(
minBridgeAmountQuote.feeAmount,
minAmountOriginQueryTokenOutInfo.decimals
)

return res.json({
maxOriginAmount,
minOriginAmount,
})
} catch (err) {
res.status(500).json({
error:
'An unexpected error occurred in /bridgeLimits. Please try again later.',
details: err.message,
})
}
}
138 changes: 138 additions & 0 deletions packages/rest-api/src/routes/bridgeLimitsRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import express from 'express'
import { check } from 'express-validator'

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

const router = express.Router()

/**
* @openapi
* /bridgeLimits:
* get:
* summary: Get min/max origin values for bridge quote
* description: Retrieve min/max bridgeable amounts to bridge from source chain to destination chain. Returns null for min/max amounts if limits are unavailable.
* parameters:
* - in: query
* name: fromChain
* required: true
* schema:
* type: integer
* description: The source chain ID.
* - in: query
* name: toChain
* required: true
* schema:
* type: integer
* description: The destination chain ID.
* - in: query
* name: fromToken
* required: true
* schema:
* type: string
* description: The address of the token on the source chain.
* - in: query
* name: toToken
* required: true
* schema:
* type: string
* description: The address of the token on the destination chain.
* responses:
* 200:
* description: Successful response containing min and max origin amounts.
* content:
* application/json:
* schema:
* type: object
* properties:
* maxOriginAmount:
* type: string
* description: Maximum amount of tokens that can be bridged from the origin chain.
* minOriginAmount:
* type: string
* description: Minimum amount of tokens that can be bridged from the origin chain.
* example:
* maxOriginAmount: "999600"
* minOriginAmount: "4"
* 400:
* description: Invalid input
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: object
* properties:
* value:
* type: string
* message:
* type: string
* field:
* type: string
* location:
* type: string
* example:
* error:
* value: "999"
* message: "Unsupported fromChain"
* field: "fromChain"
* location: "query"
* 500:
* description: Server error
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* details:
* type: string
*/
router.get(
'/',
normalizeNativeTokenAddress(['fromToken', 'toToken']),
checksumAddresses(['fromToken', 'toToken']),
[
check('fromChain')
.exists()
.withMessage('fromChain is required')
.isNumeric()
.custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value)))
.withMessage('Unsupported fromChain'),
check('toChain')
.exists()
.withMessage('toChain is required')
.isNumeric()
.custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value)))
.withMessage('Unsupported toChain'),
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'),
],
showFirstValidationError,
bridgeLimitsController
)

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 @@ -10,6 +10,7 @@ import bridgeTxStatusRoute from './bridgeTxStatusRoute'
import destinationTxRoute from './destinationTxRoute'
import tokenListRoute from './tokenListRoute'
import destinationTokensRoute from './destinationTokensRoute'
import bridgeLimitsRoute from './bridgeLimitsRoute'

const router = express.Router()

Expand All @@ -18,6 +19,7 @@ router.use('/swap', swapRoute)
router.use('/swapTxInfo', swapTxInfoRoute)
router.use('/bridge', bridgeRoute)
router.use('/bridgeTxInfo', bridgeTxInfoRoute)
router.use('/bridgeLimits', bridgeLimitsRoute)
router.use('/synapseTxId', synapseTxIdRoute)
router.use('/bridgeTxStatus', bridgeTxStatusRoute)
router.use('/destinationTx', destinationTxRoute)
Expand Down
81 changes: 81 additions & 0 deletions packages/rest-api/src/tests/bridgeLimitsRoute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import request from 'supertest'
import express from 'express'

import bridgeLimitsRoute from '../routes/bridgeLimitsRoute'
import { USDC, ETH } from '../constants/bridgeable'

const app = express()
app.use('/bridgeLimits', bridgeLimitsRoute)

describe('Get Bridge Limits Route', () => {
it('should return min/max origin amounts bridging USDC', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: 1,
fromToken: USDC.addresses[1],
toChain: 10,
toToken: USDC.addresses[10],
})

expect(response.status).toBe(200)
expect(response.body).toHaveProperty('maxOriginAmount')
expect(response.body).toHaveProperty('minOriginAmount')
}, 10_000)

it('should return min/max origin amounts bridging ETH', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: 1,
fromToken: ETH.addresses[1],
toChain: 10,
toToken: ETH.addresses[10],
})

expect(response.status).toBe(200)
expect(response.body).toHaveProperty('maxOriginAmount')
expect(response.body).toHaveProperty('minOriginAmount')
}, 10_000)

it('should return 400 for unsupported fromChain', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: '999',
toChain: '137',
fromToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff',
toToken: USDC.addresses[137],
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty(
'message',
'Unsupported fromChain'
)
}, 10_000)

it('should return 400 for unsupported toChain', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: '137',
toChain: '999',
fromToken: USDC.addresses[137],
toToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff',
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty('message', 'Unsupported toChain')
}, 10_000)

it('should return 400 for missing fromToken', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: '1',
toChain: '137',
toToken: USDC.addresses[137],
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty('field', 'fromToken')
}, 10_000)

it('should return 400 for missing toToken', async () => {
const response = await request(app).get('/bridgeLimits').query({
fromChain: '1',
toChain: '137',
fromToken: USDC.addresses[1],
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty('field', 'toToken')
}, 10_000)
})

0 comments on commit 98362bb

Please sign in to comment.