{description}
{id}
+ {siteConfig.tagline}
-{siteConfig.tagline}
+//=18.17.0"
@@ -38,7 +38,7 @@
"recharts": "^2.3.2",
"sharp": "^0.31.3",
"swr": "^1.3.0",
- "synapse-constants": "^1.3.24",
+ "synapse-constants": "^1.5.6",
"tailwind-merge": "^1.3.0",
"tiny-warning": "^1.0.3",
"web-vitals": "^2.1.4"
diff --git a/packages/explorer-ui/pages/tx/[kappa].tsx b/packages/explorer-ui/pages/tx/[kappa].tsx
index 05ac17ca7e..75bfb80b9a 100644
--- a/packages/explorer-ui/pages/tx/[kappa].tsx
+++ b/packages/explorer-ui/pages/tx/[kappa].tsx
@@ -10,7 +10,7 @@ import { API_URL } from '@graphql'
import { HorizontalDivider } from '@components/misc/HorizontalDivider'
import { formatDateTimestamp } from '@utils/formatDate'
import { IconAndAmount } from '@components/misc/IconAndAmount'
-
+import { addressToSymbol } from '@utils/addressToSymbol'
const CHAINS_BY_ID = CHAINS.CHAINS_BY_ID
const link = new HttpLink({
@@ -157,7 +157,10 @@ export const BridgeTransaction = ({ queryResult }) => {
value={fromInfo.value}
tokenAddress={fromInfo.tokenAddress}
chainId={fromInfo.chainID}
- tokenSymbol={fromInfo.tokenSymbol}
+ tokenSymbol={addressToSymbol({
+ tokenAddress: fromInfo.tokenAddress,
+ chainId: fromInfo.chainID,
+ })}
iconSize="w-4 h-4"
// textSize="text-sm"
// styledCoin={true}
@@ -183,7 +186,10 @@ export const BridgeTransaction = ({ queryResult }) => {
value={toInfo.value}
tokenAddress={toInfo.tokenAddress}
chainId={toInfo.chainID}
- tokenSymbol={toInfo.tokenSymbol}
+ tokenSymbol={addressToSymbol({
+ tokenAddress: toInfo.tokenAddress,
+ chainId: toInfo.chainID,
+ })}
iconSize="w-4 h-4"
// textSize="text-sm"
// styledCoin={true}
diff --git a/packages/rest-api/CHANGELOG.md b/packages/rest-api/CHANGELOG.md
index f65c063354..1c017e63fa 100644
--- a/packages/rest-api/CHANGELOG.md
+++ b/packages/rest-api/CHANGELOG.md
@@ -3,6 +3,79 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.3.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.3.3...@synapsecns/rest-api@1.3.4) (2024-09-29)
+
+**Note:** Version bump only for package @synapsecns/rest-api
+
+
+
+
+
+## [1.3.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.3.2...@synapsecns/rest-api@1.3.3) (2024-09-26)
+
+**Note:** Version bump only for package @synapsecns/rest-api
+
+
+
+
+
+## [1.3.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.3.1...@synapsecns/rest-api@1.3.2) (2024-09-26)
+
+**Note:** Version bump only for package @synapsecns/rest-api
+
+
+
+
+
+## [1.3.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.3.0...@synapsecns/rest-api@1.3.1) (2024-09-26)
+
+**Note:** Version bump only for package @synapsecns/rest-api
+
+
+
+
+
+# [1.3.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.2.0...@synapsecns/rest-api@1.3.0) (2024-09-25)
+
+
+### Features
+
+* **rest-api:** Adds validateRouteExists validation [SLT-260] ([#3180](https://github.com/synapsecns/sanguine/issues/3180)) ([ceff8bc](https://github.com/synapsecns/sanguine/commit/ceff8bcbc179dd6442cd1861eb31505217f9c55c))
+
+
+
+
+
+# [1.2.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.5...@synapsecns/rest-api@1.2.0) (2024-09-24)
+
+
+### Features
+
+* **api:** bridge limits [SLT-165] ([#3179](https://github.com/synapsecns/sanguine/issues/3179)) ([98362bb](https://github.com/synapsecns/sanguine/commit/98362bb7cd8972d83d7a628b2db4fb06831871c0))
+
+
+
+
+
+## [1.1.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.4...@synapsecns/rest-api@1.1.5) (2024-09-23)
+
+
+### Bug Fixes
+
+* formatted bridge fee amount ([#3165](https://github.com/synapsecns/sanguine/issues/3165)) ([b6651b1](https://github.com/synapsecns/sanguine/commit/b6651b1a19fbb6341ddf4bb3303d7aa9fa6f616e))
+
+
+
+
+
+## [1.1.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.3...@synapsecns/rest-api@1.1.4) (2024-09-21)
+
+**Note:** Version bump only for package @synapsecns/rest-api
+
+
+
+
+
## [1.1.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/rest-api@1.1.2...@synapsecns/rest-api@1.1.3) (2024-09-20)
**Note:** Version bump only for package @synapsecns/rest-api
diff --git a/packages/rest-api/README.md b/packages/rest-api/README.md
index baab635b4b..3b83d587dd 100644
--- a/packages/rest-api/README.md
+++ b/packages/rest-api/README.md
@@ -1,81 +1,77 @@
-# Swap/Bridge REST API Quoter
+# Synapse REST API
+To make requests, use https://api.synapseprotocol.com/
-To run locally:
-\`npm start\`
-
-To make requests, use https://synapse-rest-api-v2.herokuapp.com/
-The Synapse Rest API supports four main functions
+To run locally:
+```bash
+yarn dev
+```
-## /swap
+# Documentation
+[Swagger Documentation](https://api.synapseprotocol.com/api-docs/)
-which returns the following
+[GitBook Documentation](https://docs.synapseprotocol.com/developers/rest-api)
-- \`routerAddress\` (string) - The address of the router contract
-- \`maxAmountOut\` (object) - The maximum amount of tokens that can be swapped out. Contains:
- - \`type\` (string) - The data type
- - \`hex\` (string) - The amount encoded in hexidecimal
-- \`query\` (object) - Parameters for the swap query:
- - \`0\` (string) - Router contract address
- - \`1\` (string) - Address of tokenIn
- - \`2\` (object) - Amount of tokenIn to swap (same structure as maxAmountOut)
- - \`3\` (object) - Minimum amount of tokenOut requested (same structure as maxAmountOut)
- - \`4\` (string) - Encoded params for swap routing
- - \`swapAdapter\` (string) - Address of the swap adapter contract
- - \`tokenOut\` (string) - Address of tokenOut
- - \`minAmountOut\` (object) - Minimum amount of tokenOut required (same structure as maxAmountOut)
- - \`deadline\` (object) - Deadline parameter for the swap (same structure as maxAmountOut)
- - \`rawParams\` (string) - Encoded hex string containing swap parameters
-- \`maxAmountOutStr\` (string) - The maxAmountOut value formatted as a decimal string
+## REST API
+The Synapse REST API provides a set of endpoints for quoting and executing cross-chain token swaps and bridges. It supports various functions including swap quotes, bridge quotes, transaction information retrieval, and token list management.
-All \`/swap\` requests should be formatted like such:
+## Formatting Requests
+Here's an example of how to format a bridge request:
-\`/swap?chain=1&fromToken=USDC&toToken=DAI&amount=100\`
+```bash
+/bridge?fromChain=1&toChain=42161&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&toToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=100
+```
-## /bridge
+To use this in a basic application:
-which returns all transaction information
+```javascript
+async function getBridgeQuote() {
+ const response = await fetch('https://api.synapseprotocol.com/bridge?fromChain=1&toChain=42161&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&toToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=100');
+ const data = await response.json();
+ console.log(data);
+}
+getBridgeQuote();
+```
-- \`feeAmount\` (object) - The fee amount for the swap. Contains:
- - \`type\` (string) - Data type
- - \`hex\` (string) - Fee amount encoded in hex
-- \`feeConfig\` (array) - Fee configuration parameters, contains:
- - \`0\` (number) - Gas price
- - \`1\` (object) - Fee percentage denominator (hex encoded BigNumber)
- - \`2\` (object) - Protocol fee percentage numerator (hex encoded BigNumber)
-- \`routerAddress\` (string) - Address of the router contract
-- \`maxAmountOut\` (object) - Maximum amount receivable from swap, structure same as above
-- \`originQuery\` (object) - Original swap query parameters, contains:
- - \`swapAdapter\` (string) - Swap adapter address
- - \`tokenOut\` (string) - Address of output token
- - \`minAmountOut\` (object) - Minimum output token amount
- - \`deadline\` (object) - Expiry time
- - \`rawParams\` (string) - Encoded hex params
-- \`destQuery\` (object) - Destination swap query parameters, structure similar to originQuery above.
-- \`maxAmountOutStr\` (string) - maxAmountOut as a decimal string.
-All \`/bridge\` requests should be formatted like such:
+## Bridging
-\`/bridge?fromChain=1&toChain=42161&fromToken=USDC&toToken=USDC&amount=1000000\`
+1. Get a Bridge Quote to confirm the expected amount out and the bridge route
-## /swapTxInfo
+```javascript
+const quoteResponse = await fetch('https://api.synapseprotocol.com/bridge?fromChain=1&toChain=42161&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&toToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=100');
+const quoteData = await quoteResponse.json();
+```
-which returns the following
+2. Get the structured transaction information
-- \`'data'\`: The binary data that forms the input to the transaction.
-- \`'to'\`: The address of the Synapse Router (the synapse bridge contract)
+```javascript
+const txInfoResponse = await fetch('https://api.synapseprotocol.com/bridgeTxInfo?fromChain=1&toChain=42161&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&toToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=100&destAddress=0xcc78d2f004c9de9694ff6a9bbdee4793d30f3842');
+const txInfoData = await txInfoResponse.json();
+```
-All \`/swapTxInfo\` requests should be formatted like such:
+3. Execute the transaction
-\`/swap?chain=1&fromToken=USDC&toToken=DAI&amount=100\`
+```javascript
+const provider = new ethers.providers.Web3Provider(window.ethereum);
+const signer = provider.getSigner();
+const transaction = await signer.sendTransaction(txInfoData);
+const receipt = await transaction.wait();
+```
-## /bridgeTxInfo
+## Other Functions:
+1. `/destinationTokens`: This endpoint provides information about possible destination tokens for a given source chain and token. It's useful for showing users their options when initiating a cross-chain transfer.
-which returns the following
+```javascript
+const response = await fetch('https://api.synapseprotocol.com/destinationTokens?fromChain=1&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48');
+const destinationTokens = await response.json();
+```
-- \`'data'\`: The binary data that forms the input to the transaction.
-- \`'to'\`: The address of the Synapse Router (the synapse bridge contract)
+2. `/bridgeLimits`: This endpoint returns the minimum and maximum amounts that can be bridged for a specific token pair. It's helpful for validating user input and displaying available limits.
-All \`/bridgeTxInfo\` requests should be formatted like such:
+```javascript
+const limitsResponse = await fetch('https://api.synapseprotocol.com/bridgeLimits?fromChain=1&toChain=42161&fromToken=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&toToken=0xaf88d065e77c8cC2239327C5EDb3A432268e5831');
+const limitsData = await limitsResponse.json();
+```
-\`/bridgeTxInfo?fromChain=1&toChain=42161&fromToken=USDC&toToken=USDC&amount=1000000&destAddress=0xcc78d2f004c9de9694ff6a9bbdee4793d30f3842\`
+There are other additional functions that are included and documented in the Swagger documentation. Suggested changes to the API can be made by creating a new branch, making the changes, and opening a pull request. Any changes should include the appropriate test coverage and update the relevant documentation (this README, Swagger, and GitBook).
diff --git a/packages/rest-api/package.json b/packages/rest-api/package.json
index 10db70e669..fa41f97411 100644
--- a/packages/rest-api/package.json
+++ b/packages/rest-api/package.json
@@ -1,6 +1,6 @@
{
"name": "@synapsecns/rest-api",
- "version": "1.1.3",
+ "version": "1.3.4",
"private": "true",
"engines": {
"node": ">=18.17.0"
@@ -18,10 +18,11 @@
"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",
- "@synapsecns/sdk-router": "^0.11.1",
+ "@synapsecns/sdk-router": "^0.11.2",
"bignumber": "^1.1.0",
"ethers": "5.7.2",
"express": "^4.18.2",
diff --git a/packages/rest-api/src/constants/bridgeable.ts b/packages/rest-api/src/constants/bridgeable.ts
index e0bbd61d80..1a9b25a99c 100644
--- a/packages/rest-api/src/constants/bridgeable.ts
+++ b/packages/rest-api/src/constants/bridgeable.ts
@@ -1,6 +1,7 @@
import { BridgeableToken } from '../types'
import { CHAINS } from './chains'
-import { ZeroAddress } from '.'
+
+const NativeTokenAddress = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
export const GOHM: BridgeableToken = {
addresses: {
@@ -796,16 +797,16 @@ export const NETH: BridgeableToken = {
export const ETH: BridgeableToken = {
addresses: {
- [CHAINS.ETHEREUM.id]: ZeroAddress,
- [CHAINS.OPTIMISM.id]: ZeroAddress,
- [CHAINS.BOBA.id]: ZeroAddress,
+ [CHAINS.ETHEREUM.id]: NativeTokenAddress,
+ [CHAINS.OPTIMISM.id]: NativeTokenAddress,
+ [CHAINS.BOBA.id]: NativeTokenAddress,
[CHAINS.CANTO.id]: '0x5FD55A1B9FC24967C4dB09C513C3BA0DFa7FF687',
- [CHAINS.BASE.id]: ZeroAddress,
- [CHAINS.ARBITRUM.id]: ZeroAddress,
+ [CHAINS.BASE.id]: NativeTokenAddress,
+ [CHAINS.ARBITRUM.id]: NativeTokenAddress,
[CHAINS.DFK.id]: '0xfBDF0E31808d0aa7b9509AA6aBC9754E48C58852',
- [CHAINS.BLAST.id]: ZeroAddress,
- [CHAINS.SCROLL.id]: ZeroAddress,
- [CHAINS.LINEA.id]: ZeroAddress,
+ [CHAINS.BLAST.id]: NativeTokenAddress,
+ [CHAINS.SCROLL.id]: NativeTokenAddress,
+ [CHAINS.LINEA.id]: NativeTokenAddress,
},
decimals: {
[CHAINS.ETHEREUM.id]: 18,
@@ -832,7 +833,7 @@ export const ETH: BridgeableToken = {
export const MOVR: BridgeableToken = {
addresses: {
[CHAINS.MOONBEAM.id]: '0x1d4C2a246311bB9f827F4C768e277FF5787B7D7E',
- [CHAINS.MOONRIVER.id]: ZeroAddress,
+ [CHAINS.MOONRIVER.id]: NativeTokenAddress,
},
decimals: {
[CHAINS.MOONBEAM.id]: 18,
@@ -852,7 +853,7 @@ export const AVAX: BridgeableToken = {
addresses: {
[CHAINS.MOONBEAM.id]: '0xA1f8890E39b4d8E33efe296D698fe42Fb5e59cC3',
[CHAINS.KLAYTN.id]: '0xCd8fE44A29Db9159dB36f96570d7A4d91986f528',
- [CHAINS.AVALANCHE.id]: ZeroAddress,
+ [CHAINS.AVALANCHE.id]: NativeTokenAddress,
[CHAINS.DFK.id]: '0xB57B60DeBDB0b8172bb6316a9164bd3C695F133a',
[CHAINS.HARMONY.id]: '0xb12c13e66AdE1F72f71834f2FC5082Db8C091358',
},
@@ -909,7 +910,7 @@ export const WAVAX: BridgeableToken = {
export const JEWEL: BridgeableToken = {
addresses: {
- [CHAINS.DFK.id]: ZeroAddress,
+ [CHAINS.DFK.id]: NativeTokenAddress,
[CHAINS.HARMONY.id]: '0x72cb10c6bfa5624dd07ef608027e366bd690048f',
[CHAINS.KLAYTN.id]: '0x30C103f8f5A3A732DFe2dCE1Cc9446f545527b43',
[CHAINS.AVALANCHE.id]: '0x997Ddaa07d716995DE90577C123Db411584E5E46',
@@ -1158,7 +1159,7 @@ export const DAIe: BridgeableToken = {
export const KLAY: BridgeableToken = {
addresses: {
- [CHAINS.KLAYTN.id]: ZeroAddress,
+ [CHAINS.KLAYTN.id]: NativeTokenAddress,
[CHAINS.DFK.id]: '0x97855Ba65aa7ed2F65Ed832a776537268158B78a',
},
decimals: {
@@ -1194,7 +1195,7 @@ export const WKLAY: BridgeableToken = {
export const MATIC: BridgeableToken = {
addresses: {
- [CHAINS.POLYGON.id]: ZeroAddress,
+ [CHAINS.POLYGON.id]: NativeTokenAddress,
[CHAINS.DFK.id]: '0xD17a41Cd199edF1093A9Be4404EaDe52Ec19698e',
},
decimals: {
@@ -1230,7 +1231,7 @@ export const WMATIC: BridgeableToken = {
export const FTM: BridgeableToken = {
addresses: {
- [CHAINS.FANTOM.id]: ZeroAddress,
+ [CHAINS.FANTOM.id]: NativeTokenAddress,
[CHAINS.DFK.id]: '0x2Df041186C844F8a2e2b63F16145Bc6Ff7d23E25',
},
decimals: {
diff --git a/packages/rest-api/src/controllers/bridgeController.ts b/packages/rest-api/src/controllers/bridgeController.ts
index 7bb703631b..f69c8715f0 100644
--- a/packages/rest-api/src/controllers/bridgeController.ts
+++ b/packages/rest-api/src/controllers/bridgeController.ts
@@ -11,7 +11,14 @@ export const bridgeController = async (req, res) => {
return res.status(400).json({ errors: errors.array() })
}
try {
- const { fromChain, toChain, amount, fromToken, toToken } = req.query
+ const {
+ fromChain,
+ toChain,
+ amount,
+ fromToken,
+ toToken,
+ originUserAddress,
+ } = req.query
const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken)
const toTokenInfo = tokenAddressToToken(toChain.toString(), toToken)
@@ -23,19 +30,29 @@ export const bridgeController = async (req, res) => {
Number(toChain),
fromToken,
toToken,
- amountInWei
+ amountInWei,
+ originUserAddress
+ ? { originUserAddress: originUserAddress.toString() }
+ : {}
)
- const payload = resp.map((quote) => ({
- ...quote,
- maxAmountOutStr: formatBNToString(
- quote.maxAmountOut,
- toTokenInfo.decimals
- ),
- bridgeFeeFormatted: formatBNToString(
- quote.feeAmount,
- toTokenInfo.decimals
- ),
- }))
+
+ const payload = resp.map((quote) => {
+ const originQueryTokenOutInfo = tokenAddressToToken(
+ fromChain.toString(),
+ quote.originQuery.tokenOut
+ )
+ return {
+ ...quote,
+ maxAmountOutStr: formatBNToString(
+ quote.maxAmountOut,
+ toTokenInfo.decimals
+ ),
+ bridgeFeeFormatted: formatBNToString(
+ quote.feeAmount,
+ originQueryTokenOutInfo.decimals
+ ),
+ }
+ })
res.json(payload)
} catch (err) {
res.status(500).json({
diff --git a/packages/rest-api/src/controllers/bridgeLimitsController.ts b/packages/rest-api/src/controllers/bridgeLimitsController.ts
new file mode 100644
index 0000000000..1ef292338f
--- /dev/null
+++ b/packages/rest-api/src/controllers/bridgeLimitsController.ts
@@ -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,
+ })
+ }
+}
diff --git a/packages/rest-api/src/controllers/bridgeTxInfoController.ts b/packages/rest-api/src/controllers/bridgeTxInfoController.ts
index 0a0b4bc7bc..01a56f01ca 100644
--- a/packages/rest-api/src/controllers/bridgeTxInfoController.ts
+++ b/packages/rest-api/src/controllers/bridgeTxInfoController.ts
@@ -11,8 +11,15 @@ export const bridgeTxInfoController = async (req, res) => {
}
try {
- const { fromChain, toChain, amount, destAddress, fromToken, toToken } =
- req.query
+ const {
+ fromChain,
+ toChain,
+ amount,
+ destAddress,
+ fromToken,
+ toToken,
+ originUserAddress,
+ } = req.query
const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken)
@@ -23,7 +30,10 @@ export const bridgeTxInfoController = async (req, res) => {
Number(toChain),
fromToken,
toToken,
- amountInWei
+ amountInWei,
+ originUserAddress
+ ? { originUserAddress: originUserAddress.toString() }
+ : {}
)
const txInfoArray = await Promise.all(
diff --git a/packages/rest-api/src/controllers/destinationTxController.ts b/packages/rest-api/src/controllers/destinationTxController.ts
index 8db51772a2..bedc02c7f6 100644
--- a/packages/rest-api/src/controllers/destinationTxController.ts
+++ b/packages/rest-api/src/controllers/destinationTxController.ts
@@ -2,6 +2,7 @@ import { validationResult } from 'express-validator'
import { ethers } from 'ethers'
import { getTokenDecimals } from '../utils/getTokenDecimals'
+import { tokenAddressToToken } from '../utils/tokenAddressToToken'
export const destinationTxController = async (req, res) => {
const errors = validationResult(req)
@@ -47,15 +48,18 @@ export const destinationTxController = async (req, res) => {
const toInfo = graphqlData.data.bridgeTransactions[0]?.toInfo || null
if (toInfo) {
- const { tokenAddress, value, ...restToInfo } = toInfo
+ const { tokenAddress, value, chainID, ...restToInfo } = toInfo
- const tokenDecimals = getTokenDecimals(toInfo.chainID, tokenAddress)
+ const tokenInfo = tokenAddressToToken(chainID.toString(), tokenAddress)
+ const tokenDecimals = getTokenDecimals(chainID, tokenAddress)
const formattedValue = ethers.utils.formatUnits(value, tokenDecimals)
res.json({
status: 'completed',
toInfo: {
+ chainID,
...restToInfo,
+ tokenSymbol: tokenInfo ? tokenInfo?.symbol : null,
formattedValue: `${formattedValue}`,
},
})
diff --git a/packages/rest-api/src/middleware/normalizeNativeTokenAddress.ts b/packages/rest-api/src/middleware/normalizeNativeTokenAddress.ts
new file mode 100644
index 0000000000..546c08136e
--- /dev/null
+++ b/packages/rest-api/src/middleware/normalizeNativeTokenAddress.ts
@@ -0,0 +1,18 @@
+import { Request, Response, NextFunction } from 'express'
+import { isAddress, getAddress } from 'ethers/lib/utils'
+
+import { NativeGasAddress, ZeroAddress } from '../constants'
+
+export const normalizeNativeTokenAddress = (addressFields: string[]) => {
+ return (req: Request, _res: Response, next: NextFunction) => {
+ for (const field of addressFields) {
+ const address = req.query[field]
+ if (typeof address === 'string' && isAddress(address)) {
+ const checksumAddress = getAddress(address)
+ req.query[field] =
+ checksumAddress === ZeroAddress ? NativeGasAddress : checksumAddress
+ }
+ }
+ next()
+ }
+}
diff --git a/packages/rest-api/src/routes/bridgeLimitsRoute.ts b/packages/rest-api/src/routes/bridgeLimitsRoute.ts
new file mode 100644
index 0000000000..392937ac28
--- /dev/null
+++ b/packages/rest-api/src/routes/bridgeLimitsRoute.ts
@@ -0,0 +1,146 @@
+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'
+import { validateRouteExists } from '../validations/validateRouteExists'
+
+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'),
+ 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'),
+ ],
+ showFirstValidationError,
+ bridgeLimitsController
+)
+
+export default router
diff --git a/packages/rest-api/src/routes/bridgeRoute.ts b/packages/rest-api/src/routes/bridgeRoute.ts
index 3ee858fcb3..5c5d7b6c65 100644
--- a/packages/rest-api/src/routes/bridgeRoute.ts
+++ b/packages/rest-api/src/routes/bridgeRoute.ts
@@ -1,5 +1,6 @@
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'
@@ -7,6 +8,8 @@ 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'
const router = express.Router()
@@ -47,6 +50,12 @@ const router = express.Router()
* 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
* responses:
* 200:
* description: Successful response
@@ -187,6 +196,7 @@ const router = express.Router()
*/
router.get(
'/',
+ normalizeNativeTokenAddress(['fromToken', 'toToken']),
checksumAddresses(['fromToken', 'toToken']),
[
check('fromChain')
@@ -220,6 +230,17 @@ router.get(
)
.withMessage('Token not supported on specified chain'),
check('amount').isNumeric().exists().withMessage('amount is required'),
+ 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'),
],
showFirstValidationError,
bridgeController
diff --git a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts
index 4336711511..44b6904e08 100644
--- a/packages/rest-api/src/routes/bridgeTxInfoRoute.ts
+++ b/packages/rest-api/src/routes/bridgeTxInfoRoute.ts
@@ -8,6 +8,8 @@ import { bridgeTxInfoController } from '../controllers/bridgeTxInfoController'
import { isTokenAddress } from '../utils/isTokenAddress'
import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain'
import { checksumAddresses } from '../middleware/checksumAddresses'
+import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress'
+import { validateRouteExists } from '../validations/validateRouteExists'
const router = express.Router()
@@ -54,6 +56,12 @@ const router = express.Router()
* schema:
* type: string
* description: The destination address for the bridged tokens
+ * - in: query
+ * name: originUserAddress
+ * required: false
+ * schema:
+ * type: string
+ * description: The address of the user on the origin chain
* responses:
* 200:
* description: Successful response
@@ -123,6 +131,7 @@ const router = express.Router()
*/
router.get(
'/',
+ normalizeNativeTokenAddress(['fromToken', 'toToken']),
checksumAddresses(['fromToken', 'toToken']),
[
check('fromChain')
@@ -161,6 +170,17 @@ router.get(
.withMessage('destAddress is required')
.custom((value) => isAddress(value))
.withMessage('Invalid destination address'),
+ 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'),
],
showFirstValidationError,
bridgeTxInfoController
diff --git a/packages/rest-api/src/routes/destinationTokensRoute.ts b/packages/rest-api/src/routes/destinationTokensRoute.ts
index f06ac9e4f6..a6e4c590d4 100644
--- a/packages/rest-api/src/routes/destinationTokensRoute.ts
+++ b/packages/rest-api/src/routes/destinationTokensRoute.ts
@@ -8,6 +8,7 @@ import { destinationTokensController } from '../controllers/destinationTokensCon
import { isTokenAddress } from '../utils/isTokenAddress'
import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain'
import { checksumAddresses } from '../middleware/checksumAddresses'
+import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress'
const router = express.Router()
@@ -98,6 +99,7 @@ const router = express.Router()
router.get(
'/',
+ normalizeNativeTokenAddress(['fromToken']),
checksumAddresses(['fromToken']),
[
check('fromChain')
diff --git a/packages/rest-api/src/routes/index.ts b/packages/rest-api/src/routes/index.ts
index 1bbcf3ea51..2c5e1c547e 100644
--- a/packages/rest-api/src/routes/index.ts
+++ b/packages/rest-api/src/routes/index.ts
@@ -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()
@@ -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)
diff --git a/packages/rest-api/src/routes/swapRoute.ts b/packages/rest-api/src/routes/swapRoute.ts
index ee65c015b0..8c050f3b2b 100644
--- a/packages/rest-api/src/routes/swapRoute.ts
+++ b/packages/rest-api/src/routes/swapRoute.ts
@@ -7,6 +7,7 @@ import { CHAINS_ARRAY } from '../constants/chains'
import { isTokenAddress } from '../utils/isTokenAddress'
import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain'
import { checksumAddresses } from '../middleware/checksumAddresses'
+import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress'
const router = express.Router()
@@ -133,6 +134,7 @@ const router = express.Router()
*/
router.get(
'/',
+ normalizeNativeTokenAddress(['fromToken', 'toToken']),
checksumAddresses(['fromToken', 'toToken']),
[
check('chain')
diff --git a/packages/rest-api/src/routes/swapTxInfoRoute.ts b/packages/rest-api/src/routes/swapTxInfoRoute.ts
index 2ffb03388f..093dc5686f 100644
--- a/packages/rest-api/src/routes/swapTxInfoRoute.ts
+++ b/packages/rest-api/src/routes/swapTxInfoRoute.ts
@@ -8,6 +8,7 @@ import { swapTxInfoController } from '../controllers/swapTxInfoController'
import { isTokenAddress } from '../utils/isTokenAddress'
import { isTokenSupportedOnChain } from '../utils/isTokenSupportedOnChain'
import { checksumAddresses } from '../middleware/checksumAddresses'
+import { normalizeNativeTokenAddress } from '../middleware/normalizeNativeTokenAddress'
const router = express.Router()
@@ -115,6 +116,7 @@ const router = express.Router()
*/
router.get(
'/',
+ normalizeNativeTokenAddress(['fromToken', 'toToken']),
checksumAddresses(['fromToken', 'toToken']),
[
check('chain')
diff --git a/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts b/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts
new file mode 100644
index 0000000000..9e68a5d20e
--- /dev/null
+++ b/packages/rest-api/src/tests/bridgeLimitsRoute.test.ts
@@ -0,0 +1,96 @@
+import request from 'supertest'
+import express from 'express'
+
+import bridgeLimitsRoute from '../routes/bridgeLimitsRoute'
+import { USDC, ETH } from '../constants/bridgeable'
+import { NativeGasAddress } from '../constants'
+
+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 route', async () => {
+ const response = await request(app).get('/bridgeLimits').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: NativeGasAddress,
+ toToken: USDC.addresses[10],
+ })
+ expect(response.status).toBe(400)
+ expect(response.body.error).toHaveProperty(
+ 'message',
+ 'No valid route exists for the chain/token combination'
+ )
+ })
+
+ 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'
+ )
+ })
+
+ 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')
+ })
+
+ 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')
+ })
+
+ 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')
+ })
+})
diff --git a/packages/rest-api/src/tests/bridgeRoute.test.ts b/packages/rest-api/src/tests/bridgeRoute.test.ts
index dcef8451ec..542feab63a 100644
--- a/packages/rest-api/src/tests/bridgeRoute.test.ts
+++ b/packages/rest-api/src/tests/bridgeRoute.test.ts
@@ -2,6 +2,8 @@ import request from 'supertest'
import express from 'express'
import bridgeRoute from '../routes/bridgeRoute'
+import { NativeGasAddress, ZeroAddress } from '../constants'
+import { USDC } from '../constants/bridgeable'
const app = express()
app.use('/bridge', bridgeRoute)
@@ -11,10 +13,28 @@ describe('Bridge Route with Real Synapse Service', () => {
const response = await request(app).get('/bridge').query({
fromChain: '1',
toChain: '10',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
- toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on Optimism
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[10],
amount: '1000',
})
+
+ expect(response.status).toBe(200)
+ expect(Array.isArray(response.body)).toBe(true)
+ expect(response.body.length).toBeGreaterThan(0)
+ expect(response.body[0]).toHaveProperty('maxAmountOutStr')
+ expect(response.body[0]).toHaveProperty('bridgeFeeFormatted')
+ }, 15000)
+
+ it('should return bridge quotes for valid originUserAddress', async () => {
+ const response = await request(app).get('/bridge').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[10],
+ amount: '1000',
+ originUserAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
+ })
+
expect(response.status).toBe(200)
expect(Array.isArray(response.body)).toBe(true)
expect(response.body.length).toBeGreaterThan(0)
@@ -22,6 +42,70 @@ describe('Bridge Route with Real Synapse Service', () => {
expect(response.body[0]).toHaveProperty('bridgeFeeFormatted')
}, 15000)
+ it('should return bridge quotes for ZeroAddress', async () => {
+ const response = await request(app).get('/bridge').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: ZeroAddress,
+ toToken: ZeroAddress,
+ amount: '10',
+ })
+ expect(response.status).toBe(200)
+ expect(Array.isArray(response.body)).toBe(true)
+ expect(response.body.length).toBeGreaterThan(0)
+ expect(response.body[0]).toHaveProperty('maxAmountOutStr')
+ expect(response.body[0]).toHaveProperty('bridgeFeeFormatted')
+ }, 15000)
+
+ it('should return bridge quotes for NativeGasAddress', async () => {
+ const response = await request(app).get('/bridge').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: NativeGasAddress,
+ toToken: NativeGasAddress,
+ amount: '10',
+ })
+
+ expect(response.status).toBe(200)
+ expect(Array.isArray(response.body)).toBe(true)
+ expect(response.body.length).toBeGreaterThan(0)
+ expect(response.body[0]).toHaveProperty('maxAmountOutStr')
+ expect(response.body[0]).toHaveProperty('bridgeFeeFormatted')
+ }, 15000)
+
+ it('should return 400 for invalid originUserAddress', async () => {
+ const response = await request(app).get('/bridge').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[10],
+ amount: '1000',
+ originUserAddress: 'invalid_address',
+ })
+
+ expect(response.status).toBe(400)
+ expect(response.body.error).toHaveProperty(
+ 'message',
+ 'Invalid originUserAddress address'
+ )
+ }, 15000)
+
+ it('should return 400 for unsupported route', async () => {
+ const response = await request(app).get('/bridge').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: NativeGasAddress,
+ toToken: USDC.addresses[10],
+ amount: '10',
+ })
+
+ expect(response.status).toBe(400)
+ expect(response.body.error).toHaveProperty(
+ 'message',
+ 'No valid route exists for the chain/token combination'
+ )
+ })
+
it('should return 400 for unsupported fromChain, with error message', async () => {
const response = await request(app).get('/bridge').query({
fromChain: '999',
@@ -35,7 +119,7 @@ describe('Bridge Route with Real Synapse Service', () => {
'message',
'Unsupported fromChain'
)
- }, 10000)
+ })
it('should return 400 for unsupported toChain, with error message', async () => {
const response = await request(app).get('/bridge').query({
@@ -47,7 +131,7 @@ describe('Bridge Route with Real Synapse Service', () => {
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty('message', 'Unsupported toChain')
- }, 10000)
+ })
it('should return 400 for invalid fromToken address, with error message', async () => {
const response = await request(app).get('/bridge').query({
@@ -62,7 +146,7 @@ describe('Bridge Route with Real Synapse Service', () => {
'message',
'Invalid fromToken address'
)
- }, 10000)
+ })
it('should return 400 for token not supported on specified chain, with error message', async () => {
const response = await request(app).get('/bridge').query({
@@ -77,16 +161,16 @@ describe('Bridge Route with Real Synapse Service', () => {
'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',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[10],
})
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 35ff81f19a..e39450e725 100644
--- a/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts
+++ b/packages/rest-api/src/tests/bridgeTxInfoRoute.test.ts
@@ -2,6 +2,8 @@ import request from 'supertest'
import express from 'express'
import bridgeTxInfoRoute from '../routes/bridgeTxInfoRoute'
+import { USDC } from '../constants/bridgeable'
+import { NativeGasAddress } from '../constants'
const app = express()
app.use('/bridgeTxInfo', bridgeTxInfoRoute)
@@ -11,8 +13,8 @@ describe('Bridge TX Info Route', () => {
const response = await request(app).get('/bridgeTxInfo').query({
fromChain: '1',
toChain: '137',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
- toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[137],
amount: '1000',
destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
@@ -27,6 +29,61 @@ describe('Bridge TX Info Route', () => {
)
}, 10_000)
+ it('should return bridge transaction info for valid input with valid originUserAddress', async () => {
+ const response = await request(app).get('/bridgeTxInfo').query({
+ fromChain: '1',
+ toChain: '137',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[137],
+ amount: '1000',
+ destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
+ originUserAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
+ })
+
+ expect(response.status).toBe(200)
+ expect(Array.isArray(response.body)).toBe(true)
+ expect(response.body.length).toBeGreaterThan(0)
+ expect(response.body[0]).toHaveProperty('data')
+ expect(response.body[0]).toHaveProperty(
+ 'to',
+ '0xd5a597d6e7ddf373a92C8f477DAAA673b0902F48'
+ )
+ }, 10_000)
+
+ it('should return 400 for invalid originUserAddress', async () => {
+ const response = await request(app).get('/bridgeTxInfo').query({
+ fromChain: '1',
+ toChain: '137',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[137],
+ amount: '1000',
+ destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
+ originUserAddress: 'invalid_address',
+ })
+
+ expect(response.status).toBe(400)
+ expect(response.body.error).toHaveProperty(
+ 'message',
+ 'Invalid originUserAddress address'
+ )
+ }, 10_000)
+
+ it('should return 400 for unsupported route', async () => {
+ const response = await request(app).get('/bridgeTxInfo').query({
+ fromChain: '1',
+ toChain: '10',
+ fromToken: NativeGasAddress,
+ toToken: USDC.addresses[10],
+ amount: '10',
+ destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
+ })
+ expect(response.status).toBe(400)
+ expect(response.body.error).toHaveProperty(
+ 'message',
+ 'No valid route exists for the chain/token combination'
+ )
+ })
+
it('should return 400 for unsupported fromChain', async () => {
const response = await request(app).get('/bridgeTxInfo').query({
fromChain: '999',
@@ -41,7 +98,7 @@ describe('Bridge TX Info Route', () => {
'message',
'Unsupported fromChain'
)
- }, 10_000)
+ })
it('should return 400 for invalid fromToken address', async () => {
const response = await request(app).get('/bridgeTxInfo').query({
@@ -57,7 +114,7 @@ describe('Bridge TX Info Route', () => {
'message',
'Invalid fromToken address'
)
- }, 10_000)
+ })
it('should return 400 for token not supported on specified chain', async () => {
const response = await request(app).get('/bridgeTxInfo').query({
@@ -73,26 +130,26 @@ describe('Bridge TX Info Route', () => {
'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: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[137],
destAddress: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
expect(response.status).toBe(400)
expect(response.body.error).toHaveProperty('field', 'amount')
- }, 10_000)
+ })
it('should return 400 for invalid destAddress', async () => {
const response = await request(app).get('/bridgeTxInfo').query({
fromChain: '1',
toChain: '137',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
+ fromToken: USDC.addresses[1],
+ toToken: USDC.addresses[137],
amount: '1000',
destAddress: 'invalid_address',
})
@@ -101,5 +158,5 @@ describe('Bridge TX Info Route', () => {
'message',
'Invalid destination address'
)
- }, 10_000)
+ })
})
diff --git a/packages/rest-api/src/tests/destinationTokensRoute.test.ts b/packages/rest-api/src/tests/destinationTokensRoute.test.ts
index 4fe40a7a62..a083741ffa 100644
--- a/packages/rest-api/src/tests/destinationTokensRoute.test.ts
+++ b/packages/rest-api/src/tests/destinationTokensRoute.test.ts
@@ -2,6 +2,8 @@ import request from 'supertest'
import express from 'express'
import destinationTokensRoute from '../routes/destinationTokensRoute'
+import { NativeGasAddress, ZeroAddress } from '../constants'
+import { USDC, USDT } from '../constants/bridgeable'
const app = express()
app.use('/destinationTokens', destinationTokensRoute)
@@ -10,7 +12,7 @@ describe('destinatonTokens Route', () => {
it('should return destination tokens for valid input', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
})
expect(response.status).toBe(200)
@@ -24,7 +26,21 @@ describe('destinatonTokens Route', () => {
it('should return destination tokens for valid gas Tokens', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '1',
- fromToken: '0x0000000000000000000000000000000000000000',
+ fromToken: NativeGasAddress,
+ })
+
+ 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 destination tokens for valid gas Tokens, ZeroAddress', async () => {
+ const response = await request(app).get('/destinationTokens').query({
+ fromChain: '1',
+ fromToken: ZeroAddress,
})
expect(response.status).toBe(200)
@@ -40,7 +56,7 @@ describe('destinatonTokens Route', () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '534352',
- fromToken: '0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4',
+ fromToken: USDC.addresses[534352],
})
expect(response.status).toBe(200)
@@ -54,7 +70,7 @@ describe('destinatonTokens Route', () => {
it('should return destination tokens for non-checksummed address', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '43114',
- fromToken: '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7',
+ fromToken: USDT.addresses[43114].toLowerCase(),
})
expect(response.status).toBe(200)
@@ -107,7 +123,7 @@ describe('destinatonTokens Route', () => {
it('should return 400 for token not supported on specified chain', async () => {
const response = await request(app).get('/destinationTokens').query({
fromChain: '10',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
})
expect(response.status).toBe(400)
@@ -119,7 +135,7 @@ describe('destinatonTokens Route', () => {
it('should return 400 for missing fromChain', async () => {
const response = await request(app).get('/destinationTokens').query({
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
})
expect(response.status).toBe(400)
diff --git a/packages/rest-api/src/tests/swapRoute.test.ts b/packages/rest-api/src/tests/swapRoute.test.ts
index 6c6d7ac43b..775372bc31 100644
--- a/packages/rest-api/src/tests/swapRoute.test.ts
+++ b/packages/rest-api/src/tests/swapRoute.test.ts
@@ -2,6 +2,8 @@ import request from 'supertest'
import express from 'express'
import swapRoute from '../routes/swapRoute'
+import { NativeGasAddress, ZeroAddress } from '../constants'
+import { DAI, NETH, USDC } from '../constants/bridgeable'
const app = express()
app.use('/swap', swapRoute)
@@ -10,8 +12,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: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
amount: '1000',
})
@@ -21,11 +23,39 @@ describe('Swap Route with Real Synapse Service', () => {
expect(response.body).toHaveProperty('query')
}, 10_000)
+ it('should return a real swap quote for valid input, Eth ZeroAddress', async () => {
+ const response = await request(app).get('/swap').query({
+ chain: '10',
+ fromToken: ZeroAddress,
+ toToken: NETH.addresses[10],
+ amount: '1',
+ })
+
+ expect(response.status).toBe(200)
+ expect(response.body).toHaveProperty('maxAmountOut')
+ expect(response.body).toHaveProperty('routerAddress')
+ expect(response.body).toHaveProperty('query')
+ }, 10_000)
+
+ it('should return a real swap quote for valid input, Eth NativeGasAddress', async () => {
+ const response = await request(app).get('/swap').query({
+ chain: '10',
+ fromToken: NativeGasAddress,
+ toToken: NETH.addresses[10],
+ amount: '1',
+ })
+
+ expect(response.status).toBe(200)
+ expect(response.body).toHaveProperty('maxAmountOut')
+ expect(response.body).toHaveProperty('routerAddress')
+ expect(response.body).toHaveProperty('query')
+ }, 10_000)
+
it('should return 400 for unsupported chain, with error message', async () => {
const response = await request(app).get('/swap').query({
chain: '111',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
amount: '1000',
})
@@ -36,7 +66,7 @@ describe('Swap Route with Real Synapse Service', () => {
it('should return 400 for invalid toToken address, with error message', async () => {
const response = await request(app).get('/swap').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
toToken: 'invalid_address',
amount: '1000',
})
@@ -51,7 +81,7 @@ describe('Swap Route with Real Synapse Service', () => {
it('should return 400 for token not supported on specified chain', async () => {
const response = await request(app).get('/swap').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
toToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported)
amount: '1000',
})
@@ -66,8 +96,8 @@ describe('Swap Route with Real Synapse Service', () => {
it('should return 400 for missing amount, with error message', async () => {
const response = await request(app).get('/swap').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
})
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 3faf9ca8ea..99ea0e0f42 100644
--- a/packages/rest-api/src/tests/swapTxInfoRoute.test.ts
+++ b/packages/rest-api/src/tests/swapTxInfoRoute.test.ts
@@ -2,6 +2,7 @@ import request from 'supertest'
import express from 'express'
import swapTxInfoRoute from '../routes/swapTxInfoRoute'
+import { DAI, USDC } from '../constants/bridgeable'
const app = express()
app.use('/swapTxInfo', swapTxInfoRoute)
@@ -10,8 +11,8 @@ 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: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI on Ethereum
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
amount: '1000',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
@@ -24,8 +25,8 @@ describe('Swap TX Info Route with Real Synapse Service', () => {
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',
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
amount: '1000',
address: 'invalid_address',
})
@@ -39,8 +40,8 @@ describe('Swap TX Info Route with Real Synapse Service', () => {
it('should return 400 for unsupported chain, with error message', async () => {
const response = await request(app).get('/swapTxInfo').query({
chain: '111',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
amount: '1000',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
@@ -51,7 +52,7 @@ describe('Swap TX Info Route with Real Synapse Service', () => {
it('should return 400 for invalid toToken address, with error message', async () => {
const response = await request(app).get('/swapTxInfo').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
toToken: 'invalid_address',
amount: '1000',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
@@ -66,7 +67,7 @@ describe('Swap TX Info Route with Real Synapse Service', () => {
it('should return 400 for token not supported on specified chain', async () => {
const response = await request(app).get('/swapTxInfo').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
+ fromToken: USDC.addresses[1],
toToken: '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', // SNX on Ethereum (Not supported)
amount: '1000',
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
@@ -81,8 +82,8 @@ describe('Swap TX Info Route with Real Synapse Service', () => {
it('should return 400 for missing amount, with error message', async () => {
const response = await request(app).get('/swapTxInfo').query({
chain: '1',
- fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
- toToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
+ fromToken: USDC.addresses[1],
+ toToken: DAI.addresses[1],
address: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
expect(response.status).toBe(400)
diff --git a/packages/rest-api/src/tests/tokenListRoute.test.ts b/packages/rest-api/src/tests/tokenListRoute.test.ts
index 16d2fb82df..127ea9fb12 100644
--- a/packages/rest-api/src/tests/tokenListRoute.test.ts
+++ b/packages/rest-api/src/tests/tokenListRoute.test.ts
@@ -16,7 +16,7 @@ describe('Index Route', () => {
expect(keys.length).toBe(62)
expect(response.body['ETH']['addresses']['1']).toBe(
- '0x0000000000000000000000000000000000000000'
+ '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
)
expect(response.body['SYN']['addresses']['1']).toBe(
'0x0f2d719407fdbeff09d87557abb7232601fd9f29'
diff --git a/packages/rest-api/src/utils/bridgeRouteMapping.ts b/packages/rest-api/src/utils/bridgeRouteMapping.ts
index 996652e543..992ccffe0f 100644
--- a/packages/rest-api/src/utils/bridgeRouteMapping.ts
+++ b/packages/rest-api/src/utils/bridgeRouteMapping.ts
@@ -13,7 +13,7 @@ type TransformedBridgeRoutes = Record
+rfq-indexer
+├── api: API service
+│ ├── src/ : API source code
+│ ├── package.json : API dependencies and scripts
+│ ├── README.md : API documentation
+├── indexer: Indexer service
+│ ├── src/ : Indexer source code
+│ ├── abis/ : Contract ABIs
+│ ├── package.json : Indexer dependencies and scripts
+│ ├── README.md : Indexer documentation
+
diff --git a/packages/rfq-indexer/api/CHANGELOG.md b/packages/rfq-indexer/api/CHANGELOG.md
index 748a06cf8b..2ffd88a1c8 100644
--- a/packages/rfq-indexer/api/CHANGELOG.md
+++ b/packages/rfq-indexer/api/CHANGELOG.md
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [1.0.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.3...@synapsecns/rfq-indexer-api@1.0.4) (2024-09-22)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer-api
+
+
+
+
+
## [1.0.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.2...@synapsecns/rfq-indexer-api@1.0.3) (2024-09-20)
**Note:** Version bump only for package @synapsecns/rfq-indexer-api
diff --git a/packages/rfq-indexer/api/package.json b/packages/rfq-indexer/api/package.json
index dda045192c..ca054661f8 100644
--- a/packages/rfq-indexer/api/package.json
+++ b/packages/rfq-indexer/api/package.json
@@ -1,22 +1,23 @@
{
"name": "@synapsecns/rfq-indexer-api",
"private": true,
- "version": "1.0.3",
+ "version": "1.0.4",
"description": "",
"main": "index.js",
"scripts": {
"check-env": "dotenv -e .env.local -- printenv | grep DATABASE_URL",
- "dev:local": "dotenv -e .env.local -- tsx watch src/index.ts",
- "dev:prod": "dotenv -e .env.production -- tsx watch src/index.ts",
- "start": "tsx src/index.ts",
- "start:local": "dotenv -e .env -- tsx src/index.ts",
- "dev": "dotenv -e .env -- tsx watch src/index.ts",
+ "dev:local": "dotenv -e .env.local -- tsx watch src/app.ts",
+ "dev:prod": "dotenv -e .env.production -- tsx watch src/app.ts",
+ "start": "tsx src/app.ts",
+ "start:local": "dotenv -e .env -- tsx src/app.ts",
+ "dev": "dotenv -e .env -- tsx watch src/app.ts",
"lint:check": " ",
"ci:lint": " ",
"build:go": " ",
"build": " ",
"build:slither": " ",
- "test:coverage": "echo 'No tests defined.'"
+ "test": "",
+ "test:coverage": "echo no tests defined"
},
"keywords": [],
"author": "",
@@ -29,10 +30,12 @@
"@types/node": "^22.5.4",
"dotenv-cli": "^7.4.2",
"express": "^4.21.0",
+ "express-validator": "^7.2.0",
"graphql": "^16.9.0",
"graphql-yoga": "^5.7.0",
"kysely": "^0.27.4",
"pg": "^8.12.0",
+ "supertest": "^7.0.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
@@ -42,8 +45,17 @@
"node": ">=18.17"
},
"devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/preset-env": "^7.25.4",
+ "@babel/preset-typescript": "^7.24.7",
"@types/pg": "^8.11.9",
- "dotenv": "^16.4.5"
+ "@types/supertest": "^6.0.2",
+ "@types/swagger-jsdoc": "6.0.4",
+ "@types/swagger-ui-express": "4.1.6",
+ "dotenv": "^16.4.5",
+ "express-validator": "^7.2.0",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.1"
},
"repository": {
"type": "git",
diff --git a/packages/rfq-indexer/api/src/.babelrc b/packages/rfq-indexer/api/src/.babelrc
new file mode 100644
index 0000000000..3313ff9ef0
--- /dev/null
+++ b/packages/rfq-indexer/api/src/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["@babel/preset-env", "@babel/preset-typescript"]
+}
diff --git a/packages/rfq-indexer/api/src/app.ts b/packages/rfq-indexer/api/src/app.ts
new file mode 100644
index 0000000000..4209656836
--- /dev/null
+++ b/packages/rfq-indexer/api/src/app.ts
@@ -0,0 +1,32 @@
+import express from 'express'
+import swaggerUi from 'swagger-ui-express'
+import { createYoga } from 'graphql-yoga'
+
+import { specs } from './swagger'
+import routes from './routes'
+import { schema } from './graphql/schema'
+import { overrideJsonBigIntSerialization } from './utils/overrideJsonBigIntSerialization'
+
+const app = express()
+const port = process.env.PORT || 3001
+
+overrideJsonBigIntSerialization()
+
+app.use(express.json())
+
+// Swagger UI setup
+app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))
+
+// REST API routes
+app.use('/api', routes)
+
+// GraphQL setup
+const yoga = createYoga({ schema })
+app.use('/graphql', yoga)
+
+export const server = app.listen(port, () => {
+ console.log(`Server listening at ${port}`)
+ console.info('API server runs on http://localhost:3001')
+ console.info('REST requests go through http://localhost:3001/api')
+ console.info('GraphQL requests go through http://localhost:3001/graphql')
+})
diff --git a/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
new file mode 100644
index 0000000000..5079bb0d5f
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
@@ -0,0 +1,50 @@
+import { Request, Response } from 'express'
+import { sql } from 'kysely'
+
+import { db } from '../db'
+import { qDeposits, qRelays, qProofs } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const conflictingProofsController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .where('relayer_proof', 'is not', null)
+ .where('relayer_relay', 'is not', null)
+ .where(
+ (eb) =>
+ sql