-
Notifications
You must be signed in to change notification settings - Fork 30
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
[wip] feat(api): bridge limit script #3131
Changes from 14 commits
f131926
acd653c
6ada55a
8530122
4428e8d
69f36c3
b48a36e
d4fac70
5e5e2b7
7eab9a7
f56517a
ddb245d
139e27c
95a93ca
96ec975
be863d5
c6cd501
bb9aa82
9fb48d8
d006117
8ce1097
f844897
8a8c923
07980e5
46e73c2
6696b4d
0cce954
2039353
b342984
f6246d4
a1d4650
33f5584
60138d4
64f6816
6990b0f
fdefa7e
eb7aa76
68c6c48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import axios from 'axios' | ||
import { validationResult } from 'express-validator' | ||
import { BigNumber } from 'ethers' | ||
import { parseUnits } from '@ethersproject/units' | ||
import { getAddress } from '@ethersproject/address' | ||
|
||
import { Synapse } from '../services/synapseService' | ||
import { tokenAddressToToken } from '../utils/tokenAddressToToken' | ||
|
||
export const getBridgeLimitsController = 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 rfqResponse = await axios.get('https://rfq-api.omnirpc.io/quotes', { | ||
params: { | ||
originChainId: fromChain, | ||
originTokenAddress: fromTokenInfo.address, | ||
destChainId: toChain, | ||
destTokenAddress: toTokenInfo.address, | ||
}, | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the API call to the RFQ service. The function makes an API call to the RFQ service to retrieve quotes for the specified token transfer. However, it does not handle potential errors that may occur during this API call, such as network errors or invalid responses. To improve the robustness of the function, consider adding error handling for this API call, such as catching any errors and returning an appropriate error response to the client. |
||
|
||
const rfqQuotes = rfqResponse.data | ||
|
||
let bestRfqQuote = null | ||
|
||
if (Array.isArray(rfqQuotes) && rfqQuotes.length > 0) { | ||
const filteredQuotes = rfqQuotes | ||
.slice(0, 20) | ||
.filter( | ||
(quote) => | ||
Number(quote.origin_chain_id) === Number(fromChain) && | ||
Number(quote.dest_chain_id) === Number(toChain) && | ||
getAddress(quote.origin_token_addr) === | ||
getAddress(fromTokenInfo.address) && | ||
getAddress(quote.dest_token_addr) === | ||
getAddress(toTokenInfo.address) | ||
) | ||
|
||
bestRfqQuote = filteredQuotes.reduce((maxQuote, currentQuote) => { | ||
const currentAmount = Number(currentQuote.max_origin_amount) | ||
const maxAmount = maxQuote ? Number(maxQuote.max_origin_amount) : 0 | ||
return currentAmount > maxAmount ? currentQuote : maxQuote | ||
}, null) | ||
} | ||
|
||
const upperLimitAmount = parseUnits('1000000', fromTokenInfo.decimals) | ||
const upperLimitBridgeQuotes = await Synapse.allBridgeQuotes( | ||
Number(fromChain), | ||
Number(toChain), | ||
fromTokenInfo.address, | ||
toTokenInfo.address, | ||
upperLimitAmount | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the API calls to the Synapse service. The function makes two API calls to the Synapse service to retrieve bridge quotes for the upper and lower limit amounts. However, it does not handle potential errors that may occur during these API calls, such as network errors or invalid responses. To improve the robustness of the function, consider adding error handling for these API calls, such as catching any errors and returning an appropriate error response to the client. Also applies to: 62-69 |
||
|
||
const lowerLimitAmount = parseUnits('100', fromTokenInfo.decimals) | ||
const lowerLimitBridgeQuotes = await Synapse.allBridgeQuotes( | ||
Number(fromChain), | ||
Number(toChain), | ||
fromTokenInfo.address, | ||
toTokenInfo.address, | ||
lowerLimitAmount | ||
) | ||
|
||
const bestUpperLimitSDKQuote = upperLimitBridgeQuotes[0] | ||
|
||
let maxOriginQuote | ||
|
||
const minBridgeFeeQuote = lowerLimitBridgeQuotes.reduce( | ||
(minQuote, currentQuote) => { | ||
const currentFeeAmount = currentQuote.feeAmount | ||
const minFeeAmount = minQuote ? minQuote.feeAmount : null | ||
|
||
return !minFeeAmount || currentFeeAmount.lt(minFeeAmount) | ||
? currentQuote | ||
: minQuote | ||
}, | ||
null | ||
) | ||
|
||
if (bestRfqQuote) { | ||
const bestRfqQuoteMaxAmountBN = BigNumber.from( | ||
bestRfqQuote.max_origin_amount | ||
) | ||
maxOriginQuote = bestRfqQuoteMaxAmountBN.gt( | ||
bestUpperLimitSDKQuote.maxAmountOut | ||
) | ||
? { source: 'RFQ', amount: bestRfqQuoteMaxAmountBN } | ||
: { | ||
source: bestUpperLimitSDKQuote.bridgeModuleName, | ||
amount: bestUpperLimitSDKQuote.maxAmountOut, | ||
} | ||
} else { | ||
maxOriginQuote = { | ||
source: bestUpperLimitSDKQuote.bridgeModuleName, | ||
amount: bestUpperLimitSDKQuote.maxAmountOut, | ||
} | ||
} | ||
|
||
return res.json({ | ||
maxOriginAmount: maxOriginQuote.amount, | ||
minOriginAmount: minBridgeFeeQuote.feeAmount, | ||
}) | ||
} catch (err) { | ||
res.status(500).json({ | ||
error: | ||
'An unexpected error occurred in /getBridgeLimits. Please try again later.', | ||
details: err.message, | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import express from 'express' | ||
import { check } from 'express-validator' | ||
import { isAddress } from '@ethersproject/address' | ||
|
||
import { CHAINS_ARRAY } from '../constants/chains' | ||
import { showFirstValidationError } from '../middleware/showFirstValidationError' | ||
import { getBridgeLimitsController } from '../controllers/getBridgeLimitsController' | ||
|
||
const router = express.Router() | ||
|
||
router.get( | ||
'/', | ||
[ | ||
check('fromChain') | ||
.isNumeric() | ||
.custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value))) | ||
.withMessage('Unsupported fromChain') | ||
.exists() | ||
.withMessage('originChainId is required'), | ||
check('toChain') | ||
.isNumeric() | ||
.custom((value) => CHAINS_ARRAY.some((c) => c.id === Number(value))) | ||
.withMessage('Unsupported toChain') | ||
.exists() | ||
.withMessage('toChain is required'), | ||
check('fromToken') | ||
.exists() | ||
.withMessage('fromToken is required') | ||
.custom((value) => isAddress(value)) | ||
.withMessage('Invalid fromToken address'), | ||
check('toToken') | ||
.exists() | ||
.withMessage('toToken is required') | ||
.custom((value) => isAddress(value)) | ||
.withMessage('Invalid toToken address'), | ||
], | ||
showFirstValidationError, | ||
getBridgeLimitsController | ||
) | ||
|
||
export default router |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import request from 'supertest' | ||
import express from 'express' | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rename this test file to |
||
import getBridgeLimitsRoute from '../routes/getBridgeLimitsRoute' | ||
|
||
const app = express() | ||
app.use('/getBridgeLimits', getBridgeLimitsRoute) | ||
|
||
describe('Get Bridge Limits Route', () => { | ||
it('should return min/max origin amounts for valid input', async () => { | ||
const response = await request(app).get('/getBridgeLimits').query({ | ||
fromChain: 1, | ||
fromToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', | ||
bigboydiamonds marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. import |
||
toChain: 10, | ||
toToken: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', | ||
}) | ||
|
||
expect(response.status).toBe(200) | ||
expect(response.body).toHaveProperty('maxOriginAmount') | ||
expect(response.body).toHaveProperty('minOriginAmount') | ||
}, 10_000) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add tests not just for stables but also ETH. |
||
it('should return 400 for unsupported fromChain', async () => { | ||
const response = await request(app).get('/getBridgeLimits').query({ | ||
fromChain: '999', | ||
toChain: '137', | ||
fromToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', | ||
toToken: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', | ||
}) | ||
expect(response.status).toBe(400) | ||
expect(response.body.error).toHaveProperty( | ||
'message', | ||
'Unsupported fromChain' | ||
) | ||
}, 10_000) | ||
|
||
it('should return 400 for unsupported ', async () => { | ||
const response = await request(app).get('/getBridgeLimits').query({ | ||
fromChain: '999', | ||
toChain: '137', | ||
fromToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', | ||
toToken: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', | ||
}) | ||
expect(response.status).toBe(400) | ||
expect(response.body.error).toHaveProperty( | ||
'message', | ||
'Unsupported fromChain' | ||
) | ||
}, 10_000) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incomplete test description and potential duplicate test case. The test case starting at line 37 has an incomplete description: Please update the test description to accurately reflect the scenario being tested. If this test is intended to check a different unsupported parameter (e.g., ToolsGitleaks
|
||
|
||
it('should return 400 for unsupported toChain', async () => { | ||
const response = await request(app).get('/getBridgeLimits').query({ | ||
fromChain: '137', | ||
toChain: '999', | ||
fromToken: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', | ||
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('/getBridgeLimits').query({ | ||
fromChain: '1', | ||
toChain: '137', | ||
toToken: '0x176211869cA2b568f2A7D4EE941E073a821EE1ff', | ||
}) | ||
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('/getBridgeLimits').query({ | ||
fromChain: '1', | ||
toChain: '137', | ||
fromToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', | ||
}) | ||
expect(response.status).toBe(400) | ||
expect(response.body.error).toHaveProperty('field', 'toToken') | ||
}, 10_000) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
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] | ||
if (!tokenInfo) { | ||
return null | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize Token addresses may differ in case, potentially leading to lookup failures since object keys in JavaScript are case sensitive. Consider normalizing Apply this diff to normalize const tokenInfo = chainData[tokenAddress]
+const normalizedTokenAddress = tokenAddress.toLowerCase()
+const tokenInfo = chainData[normalizedTokenAddress]
if (!tokenInfo) {
return null
} Also, ensure that the keys in
|
||
return { | ||
address: tokenAddress, | ||
symbol: tokenInfo.symbol, | ||
decimals: tokenInfo.decimals, | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specify the return type of To enhance type safety and readability, consider specifying the return type of the For example: interface TokenInfo {
address: string;
symbol: string;
decimals: number;
}
export const tokenAddressToToken = (chain: string, tokenAddress: string): TokenInfo | null => {
// function body...
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to
bridgeLimitsController