synapsecns/sanguine

View on GitHub
packages/rest-api/src/routes/bridgeRoute.ts

Summary

Maintainability
D
1 day
Test Coverage
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'
import { checksumAddresses } from '../middleware/checksumAddresses'
import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress'
import { validateRouteExists } from '../validations/validateRouteExists'
import { validateDecimals } from '../validations/validateDecimals'
import { tokenAddressToToken } from '../utils/tokenAddressToToken'
const router: express.Router = express.Router()

/**
 * @openapi
 * /bridge:
 *   get:
 *     summary: Get quotes for bridging tokens between chains
 *     description: Retrieve list of detailed bridge quotes based on origin and destination chains based on tokens and amount
 *     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
 *       - in: query
 *         name: amount
 *         required: true
 *         schema:
 *           type: number
 *         description: The amount of tokens to bridge
 *       - in: query
 *         name: originUserAddress
 *         required: false
 *         schema:
 *           type: string
 *         description: The address of the user on the origin chain
 *       - in: query
 *         name: destAddress
 *         required: false
 *         schema:
 *           type: string
 *         description: The destination address for the bridge transaction
 *     responses:
 *       200:
 *         description: Successful response
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 type: object
 *                 properties:
 *                   id:
 *                     type: string
 *                   feeAmount:
 *                     $ref: '#/components/schemas/BigNumber'
 *                   feeConfig:
 *                     type: object
 *                     properties:
 *                       bridgeFee:
 *                         type: integer
 *                       minFee:
 *                         $ref: '#/components/schemas/BigNumber'
 *                       maxFee:
 *                         $ref: '#/components/schemas/BigNumber'
 *                   routerAddress:
 *                     type: string
 *                   maxAmountOut:
 *                     $ref: '#/components/schemas/BigNumber'
 *                   originQuery:
 *                     type: object
 *                   destQuery:
 *                     type: object
 *                   estimatedTime:
 *                     type: integer
 *                   bridgeModuleName:
 *                     type: string
 *                   gasDropAmount:
 *                     $ref: '#/components/schemas/BigNumber'
 *                   originChainId:
 *                     type: integer
 *                   destChainId:
 *                     type: integer
 *                   maxAmountOutStr:
 *                     type: string
 *                   bridgeFeeFormatted:
 *                     type: string
 *                   callData:
 *                     type: object
 *                     nullable: true
 *                     properties:
 *                       to:
 *                         type: string
 *                       data:
 *                         type: string
 *                       value:
 *                         type: string
 *             example:
 *               - id: "01920c87-7f14-7cdf-90e1-e13b2d4af55f"
 *                 feeAmount:
 *                   type: "BigNumber"
 *                   hex: "0x17d78400"
 *                 feeConfig:
 *                   bridgeFee: 4000000
 *                   minFee:
 *                     type: "BigNumber"
 *                     hex: "0x3d0900"
 *                   maxFee:
 *                     type: "BigNumber"
 *                     hex: "0x17d78400"
 *                 routerAddress: "0xd5a597d6e7ddf373a92C8f477DAAA673b0902F48"
 *                 maxAmountOut:
 *                   type: "BigNumber"
 *                   hex: "0xe89bd2cb27"
 *                 originQuery:
 *                   routerAdapter: "0x0000000000000000000000000000000000000000"
 *                   tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
 *                   minAmountOut:
 *                     type: "BigNumber"
 *                     hex: "0xe8d4a51000"
 *                   deadline:
 *                     type: "BigNumber"
 *                     hex: "0x66ecb04b"
 *                   rawParams: "0x"
 *                 destQuery:
 *                   routerAdapter: "0xd5a597d6e7ddf373a92C8f477DAAA673b0902F48"
 *                   tokenOut: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
 *                   minAmountOut:
 *                     type: "BigNumber"
 *                     hex: "0xe89bd2cb27"
 *                   deadline:
 *                     type: "BigNumber"
 *                     hex: "0x66f5e873"
 *                   rawParams: "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009a2dea7b81cfe3e0011d44d41c5c5142b8d9abdf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002"
 *                 estimatedTime: 1020
 *                 bridgeModuleName: "SynapseCCTP"
 *                 gasDropAmount:
 *                   type: "BigNumber"
 *                   hex: "0x0110d9316ec000"
 *                 originChainId: 1
 *                 destChainId: 42161
 *                 maxAmountOutStr: "999046.695719"
 *                 bridgeFeeFormatted: "400"
 *       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
 *
 * components:
 *   schemas:
 *     BigNumber:
 *       type: object
 *       properties:
 *         type:
 *           type: string
 *           enum: [BigNumber]
 *         hex:
 *           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'),
    check('amount')
      .exists()
      .withMessage('amount is required')
      .isNumeric()
      .custom((value, { req }) => {
        const fromTokenInfo = tokenAddressToToken(
          req.query.fromChain,
          req.query.fromToken
        )
        return validateDecimals(value, fromTokenInfo.decimals)
      })
      .withMessage(
        'Amount has too many decimals, beyond the maximum allowed for this token'
      ),
    check()
      .custom((_value, { req }) => {
        const { fromChain, toChain, fromToken, toToken } = req.query

        return validateRouteExists(fromChain, fromToken, toChain, toToken)
      })
      .withMessage('No valid route exists for the chain/token combination'),
    check('originUserAddress')
      .optional()
      .custom((value) => isAddress(value))
      .withMessage('Invalid originUserAddress address'),
    check('destAddress')
      .optional()
      .custom((value) => isAddress(value))
      .withMessage('Invalid destAddress'),
  ],
  showFirstValidationError,
  bridgeController
)

export default router