-
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 27 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,110 @@ | ||
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' | ||
import { BRIDGE_LIMIT_MAPPING } from '../utils/bridgeLimitMapping' | ||
|
||
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 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({ | ||
BRIDGE_LIMIT_MAPPING, | ||
maxOriginAmount, | ||
minOriginAmount, | ||
}) | ||
} 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() | ||
|
||
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 swagger docs (see other routes for examples) |
||
router.get( | ||
'/', | ||
[ | ||
check('fromChain') | ||
.isNumeric() | ||
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. existence checks should come first:
|
||
.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/bridgeLimitsRoute' | ||
|
||
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,104 @@ | ||||||||||||||||
import { BRIDGE_MAP } from '../constants/bridgeMap' | ||||||||||||||||
import * as ALL_TOKENS from '../constants/bridgeable' | ||||||||||||||||
|
||||||||||||||||
const constructJSON = (swappableMap, exclusionList) => { | ||||||||||||||||
const result = {} | ||||||||||||||||
|
||||||||||||||||
// Iterate through the origin chains | ||||||||||||||||
for (const originChainId in swappableMap) { | ||||||||||||||||
for (const originTokenAddress in swappableMap[originChainId]) { | ||||||||||||||||
const originToken = swappableMap[originChainId][originTokenAddress] | ||||||||||||||||
const originKey = `${originToken.symbol}-${originChainId}` | ||||||||||||||||
|
||||||||||||||||
// Use transformPair to get token object | ||||||||||||||||
const transformedOriginToken = transformPair(originKey) | ||||||||||||||||
|
||||||||||||||||
if (!transformedOriginToken || exclusionList.includes(originKey)) { | ||||||||||||||||
continue | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Initialize origin chain and origin token with symbol and swappableType if not existing | ||||||||||||||||
if (!result[originChainId]) { | ||||||||||||||||
result[originChainId] = {} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
if (!result[originChainId][transformedOriginToken.address]) { | ||||||||||||||||
result[originChainId][transformedOriginToken.address] = { | ||||||||||||||||
symbol: transformedOriginToken.symbol, | ||||||||||||||||
swappableType: transformedOriginToken.swapableType, // Fetch swappableType | ||||||||||||||||
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. Inconsistent property name 'swappableType' vs 'swapableType' The property name is inconsistently spelled between Apply this diff to fix the inconsistency: Option 1: Rename - swapableType: token.swapableType,
+ swappableType: token.swapableType, Option 2: Rename - swappableType: transformedOriginToken.swapableType, // Fetch swappableType
+ swapableType: transformedOriginToken.swapableType, // Fetch swapableType Ensure that the property name is consistent throughout the codebase. Also applies to: 99-99 |
||||||||||||||||
routes: {}, | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Iterate through destination chains | ||||||||||||||||
for (const destinationChainId in swappableMap) { | ||||||||||||||||
if (originChainId === destinationChainId) { | ||||||||||||||||
continue | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
for (const destinationTokenAddress in swappableMap[ | ||||||||||||||||
destinationChainId | ||||||||||||||||
]) { | ||||||||||||||||
const destinationToken = | ||||||||||||||||
swappableMap[destinationChainId][destinationTokenAddress] | ||||||||||||||||
const destinationKey = `${destinationToken.symbol}-${destinationChainId}` | ||||||||||||||||
|
||||||||||||||||
// Use transformPair for destination token as well | ||||||||||||||||
const transformedDestinationToken = transformPair(destinationKey) | ||||||||||||||||
|
||||||||||||||||
if ( | ||||||||||||||||
!transformedDestinationToken || | ||||||||||||||||
exclusionList.includes(destinationKey) | ||||||||||||||||
) { | ||||||||||||||||
continue | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
// Check for bridge compatibility by comparing origin and destination symbols | ||||||||||||||||
for (const bridgeSymbol of originToken.origin) { | ||||||||||||||||
if ( | ||||||||||||||||
originToken.origin.includes(bridgeSymbol) && | ||||||||||||||||
destinationToken.destination.includes(bridgeSymbol) | ||||||||||||||||
) { | ||||||||||||||||
Comment on lines
+59
to
+63
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. Redundant condition in bridge compatibility check The condition Simplify the condition by removing the redundant check: for (const bridgeSymbol of originToken.origin) {
- if (
- originToken.origin.includes(bridgeSymbol) &&
- destinationToken.destination.includes(bridgeSymbol)
- ) {
+ if (destinationToken.destination.includes(bridgeSymbol)) {
// Initialize destination token
}
} Committable suggestion
Suggested change
|
||||||||||||||||
// Initialize destination token with symbol, minValue, maxValue if not existing | ||||||||||||||||
if ( | ||||||||||||||||
!result[originChainId][transformedOriginToken.address].routes[ | ||||||||||||||||
destinationChainId | ||||||||||||||||
] | ||||||||||||||||
) { | ||||||||||||||||
result[originChainId][transformedOriginToken.address].routes[ | ||||||||||||||||
destinationChainId | ||||||||||||||||
] = {} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
result[originChainId][transformedOriginToken.address].routes[ | ||||||||||||||||
destinationChainId | ||||||||||||||||
][transformedDestinationToken.address] = { | ||||||||||||||||
symbol: transformedDestinationToken.symbol, | ||||||||||||||||
minOriginValue: null, | ||||||||||||||||
maxOriginValue: null, | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
return result | ||||||||||||||||
} | ||||||||||||||||
Comment on lines
+6
to
+90
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 optimizing nested loops for performance The Explore ways to optimize the loops, such as:
This could enhance the efficiency of the function when dealing with extensive token maps. |
||||||||||||||||
|
||||||||||||||||
export const transformPair = (string: string): any => { | ||||||||||||||||
const [symbol, chainId] = string.split('-') | ||||||||||||||||
const token = Object.values(ALL_TOKENS).find((t) => t.routeSymbol === symbol) | ||||||||||||||||
const address = token?.addresses[chainId] | ||||||||||||||||
if (token && address) { | ||||||||||||||||
return { | ||||||||||||||||
symbol, | ||||||||||||||||
chainId, | ||||||||||||||||
address, | ||||||||||||||||
swapableType: token.swapableType, | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
export const BRIDGE_LIMIT_MAPPING = constructJSON(BRIDGE_MAP, []) |
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