Create new CollateralToCollateral route type Refactor route computation into separate file Add unit tests for route computationpull/51/head
parent
0faea83ca8
commit
1014966351
@ -0,0 +1,16 @@ |
||||
const nextJest = require('next/jest') |
||||
|
||||
const createJestConfig = nextJest({ |
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './', |
||||
}) |
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */ |
||||
const customJestConfig = { |
||||
// Add more setup options before each test is run
|
||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
} |
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig) |
@ -0,0 +1,252 @@ |
||||
import { TokenType } from '@hyperlane-xyz/hyperlane-token'; |
||||
|
||||
import { SOL_ZERO_ADDRESS } from '../../../consts/values'; |
||||
|
||||
import { computeTokenRoutes } from './fetch'; |
||||
|
||||
describe('computeTokenRoutes', () => { |
||||
it('Handles empty list', () => { |
||||
const routesMap = computeTokenRoutes([]); |
||||
expect(routesMap).toBeTruthy(); |
||||
expect(Object.values(routesMap).length).toBe(0); |
||||
}); |
||||
|
||||
it('Handles basic 3-node route', () => { |
||||
const routesMap = computeTokenRoutes([ |
||||
{ |
||||
type: TokenType.collateral, |
||||
tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
name: 'Weth', |
||||
symbol: 'WETH', |
||||
decimals: 18, |
||||
hypTokens: [ |
||||
{ |
||||
decimals: 18, |
||||
tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
}, |
||||
{ |
||||
decimals: 18, |
||||
tokenCaip19Id: 'ethereum:44787/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
}, |
||||
], |
||||
}, |
||||
]); |
||||
expect(routesMap).toEqual({ |
||||
'ethereum:5': { |
||||
'ethereum:11155111': [ |
||||
{ |
||||
type: 'collateralToSynthetic', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:5', |
||||
originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:11155111', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'ethereum:44787': [ |
||||
{ |
||||
type: 'collateralToSynthetic', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:5', |
||||
originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:44787', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
}, |
||||
'ethereum:11155111': { |
||||
'ethereum:5': [ |
||||
{ |
||||
type: 'syntheticToCollateral', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:11155111', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:5', |
||||
destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'ethereum:44787': [ |
||||
{ |
||||
type: 'syntheticToSynthetic', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:11155111', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:44787', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
}, |
||||
'ethereum:44787': { |
||||
'ethereum:5': [ |
||||
{ |
||||
type: 'syntheticToCollateral', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:44787', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:5', |
||||
destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'ethereum:11155111': [ |
||||
{ |
||||
type: 'syntheticToSynthetic', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:44787', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:11155111', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
it('Handles multi-collateral route', () => { |
||||
const routesMap = computeTokenRoutes([ |
||||
{ |
||||
type: TokenType.collateral, |
||||
tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
name: 'Weth', |
||||
symbol: 'WETH', |
||||
decimals: 18, |
||||
hypTokens: [ |
||||
{ |
||||
decimals: 18, |
||||
tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
}, |
||||
{ |
||||
decimals: 6, |
||||
tokenCaip19Id: 'sealevel:1399811151/native:PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
type: TokenType.native, |
||||
tokenCaip19Id: `sealevel:1399811151/native:${SOL_ZERO_ADDRESS}`, |
||||
routerAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
name: 'Zebec', |
||||
symbol: 'ZBC', |
||||
decimals: 6, |
||||
hypTokens: [ |
||||
{ |
||||
decimals: 18, |
||||
tokenCaip19Id: 'ethereum:11155111/erc20:0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
}, |
||||
{ |
||||
decimals: 18, |
||||
tokenCaip19Id: 'ethereum:5/erc20:0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
}, |
||||
], |
||||
}, |
||||
]); |
||||
expect(routesMap).toEqual({ |
||||
'ethereum:5': { |
||||
'ethereum:11155111': [ |
||||
{ |
||||
type: 'collateralToSynthetic', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:5', |
||||
originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:11155111', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'sealevel:1399811151': [ |
||||
{ |
||||
type: 'collateralToCollateral', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:5', |
||||
originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'sealevel:1399811151', |
||||
destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
destDecimals: 6, |
||||
}, |
||||
], |
||||
}, |
||||
'ethereum:11155111': { |
||||
'ethereum:5': [ |
||||
{ |
||||
type: 'syntheticToCollateral', |
||||
baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
originCaip2Id: 'ethereum:11155111', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'ethereum:5', |
||||
destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'sealevel:1399811151': [ |
||||
{ |
||||
type: 'syntheticToCollateral', |
||||
baseTokenCaip19Id: |
||||
'sealevel:1399811151/native:00000000000000000000000000000000000000000000', |
||||
baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
originCaip2Id: 'ethereum:11155111', |
||||
originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
originDecimals: 18, |
||||
destCaip2Id: 'sealevel:1399811151', |
||||
destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
destDecimals: 6, |
||||
}, |
||||
], |
||||
}, |
||||
'sealevel:1399811151': { |
||||
'ethereum:5': [ |
||||
{ |
||||
type: 'collateralToCollateral', |
||||
baseTokenCaip19Id: |
||||
'sealevel:1399811151/native:00000000000000000000000000000000000000000000', |
||||
baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
originCaip2Id: 'sealevel:1399811151', |
||||
originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
originDecimals: 6, |
||||
destCaip2Id: 'ethereum:5', |
||||
destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
'ethereum:11155111': [ |
||||
{ |
||||
type: 'collateralToSynthetic', |
||||
baseTokenCaip19Id: |
||||
'sealevel:1399811151/native:00000000000000000000000000000000000000000000', |
||||
baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
originCaip2Id: 'sealevel:1399811151', |
||||
originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', |
||||
originDecimals: 6, |
||||
destCaip2Id: 'ethereum:11155111', |
||||
destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
destDecimals: 18, |
||||
}, |
||||
], |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,166 @@ |
||||
import { ProtocolType } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses'; |
||||
import { logger } from '../../../utils/logger'; |
||||
import { getCaip2Id } from '../../caip/chains'; |
||||
import { |
||||
getCaip19Id, |
||||
getChainIdFromToken, |
||||
isNonFungibleToken, |
||||
parseCaip19Id, |
||||
resolveAssetNamespace, |
||||
} from '../../caip/tokens'; |
||||
import { getMultiProvider } from '../../multiProvider'; |
||||
import { AdapterFactory } from '../adapters/AdapterFactory'; |
||||
import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; |
||||
|
||||
import { RouteType, RoutesMap } from './types'; |
||||
|
||||
export async function fetchRemoteHypTokens( |
||||
baseToken: TokenMetadata, |
||||
allTokens: TokenMetadata[], |
||||
): Promise<TokenMetadataWithHypTokens> { |
||||
const { |
||||
symbol: baseSymbol, |
||||
tokenCaip19Id: baseTokenCaip19Id, |
||||
routerAddress: baseRouter, |
||||
} = baseToken; |
||||
const isNft = isNonFungibleToken(baseTokenCaip19Id); |
||||
logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseTokenCaip19Id})`); |
||||
|
||||
const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter); |
||||
|
||||
const remoteRouters = await baseAdapter.getAllRouters(); |
||||
logger.info(`Router addresses found:`, remoteRouters); |
||||
|
||||
const multiProvider = getMultiProvider(); |
||||
const hypTokens = await Promise.all( |
||||
remoteRouters.map(async (router) => { |
||||
const destMetadata = multiProvider.getChainMetadata(router.domain); |
||||
const protocol = destMetadata.protocol || ProtocolType.Ethereum; |
||||
const chainCaip2Id = getCaip2Id(protocol, multiProvider.getChainId(router.domain)); |
||||
const namespace = resolveAssetNamespace(protocol, false, isNft, true); |
||||
const formattedAddress = bytesToProtocolAddress(router.address, protocol); |
||||
const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, formattedAddress); |
||||
if (isNft) return { tokenCaip19Id, decimals: 0 }; |
||||
// Attempt to find the decimals from the token list
|
||||
const routerMetadata = allTokens.find((token) => |
||||
areAddressesEqual(formattedAddress, token.routerAddress), |
||||
); |
||||
if (routerMetadata) return { tokenCaip19Id, decimals: routerMetadata.decimals }; |
||||
// Otherwise try to query the contract
|
||||
const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( |
||||
baseTokenCaip19Id, |
||||
chainCaip2Id, |
||||
formattedAddress, |
||||
); |
||||
const metadata = await remoteAdapter.getMetadata(); |
||||
return { tokenCaip19Id, decimals: metadata.decimals }; |
||||
}), |
||||
); |
||||
return { ...baseToken, hypTokens }; |
||||
} |
||||
|
||||
// Process token list to populates routesCache with all possible token routes (e.g. router pairs)
|
||||
export function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]) { |
||||
const tokenRoutes: RoutesMap = {}; |
||||
|
||||
// Instantiate map structure
|
||||
const allChainIds = getChainsFromTokens(tokens); |
||||
for (const origin of allChainIds) { |
||||
tokenRoutes[origin] = {}; |
||||
for (const dest of allChainIds) { |
||||
if (origin === dest) continue; |
||||
tokenRoutes[origin][dest] = []; |
||||
} |
||||
} |
||||
|
||||
// Compute all possible routes, in both directions
|
||||
for (const token of tokens) { |
||||
for (const remoteHypToken of token.hypTokens) { |
||||
const { |
||||
tokenCaip19Id: baseTokenCaip19Id, |
||||
routerAddress: baseRouterAddress, |
||||
decimals: baseDecimals, |
||||
} = token; |
||||
const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id); |
||||
const { chainCaip2Id: remoteCaip2Id, address: remoteRouterAddress } = parseCaip19Id( |
||||
remoteHypToken.tokenCaip19Id, |
||||
); |
||||
const remoteDecimals = remoteHypToken.decimals; |
||||
// Check if the token list contains the dest router address, meaning it's also a base collateral token
|
||||
const isRemoteCollateral = tokensHasRouter(tokens, remoteRouterAddress); |
||||
const commonRouteProps = { baseTokenCaip19Id, baseRouterAddress }; |
||||
|
||||
// Register a route from the base to the remote
|
||||
tokenRoutes[baseChainCaip2Id][remoteCaip2Id]?.push({ |
||||
type: isRemoteCollateral |
||||
? RouteType.CollateralToCollateral |
||||
: RouteType.CollateralToSynthetic, |
||||
...commonRouteProps, |
||||
originCaip2Id: baseChainCaip2Id, |
||||
originRouterAddress: baseRouterAddress, |
||||
originDecimals: baseDecimals, |
||||
destCaip2Id: remoteCaip2Id, |
||||
destRouterAddress: remoteRouterAddress, |
||||
destDecimals: remoteDecimals, |
||||
}); |
||||
|
||||
// If the remote is not a synthetic (i.e. it's a native/collateral token with it's own config)
|
||||
// then stop here to avoid duplicate route entries.
|
||||
if (isRemoteCollateral) continue; |
||||
|
||||
// Register a route back from the synthetic remote to the base
|
||||
tokenRoutes[remoteCaip2Id][baseChainCaip2Id]?.push({ |
||||
type: RouteType.SyntheticToCollateral, |
||||
...commonRouteProps, |
||||
originCaip2Id: remoteCaip2Id, |
||||
originRouterAddress: remoteRouterAddress, |
||||
originDecimals: remoteDecimals, |
||||
destCaip2Id: baseChainCaip2Id, |
||||
destRouterAddress: baseRouterAddress, |
||||
destDecimals: baseDecimals, |
||||
}); |
||||
|
||||
// Now create routes from the remote synthetic token to all other hypTokens
|
||||
// This assumes the synthetics were all enrolled to connect to each other
|
||||
// which is the deployer's default behavior
|
||||
for (const otherHypToken of token.hypTokens) { |
||||
const { chainCaip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id( |
||||
otherHypToken.tokenCaip19Id, |
||||
); |
||||
// Skip if it's same hypToken as parent loop (no route to self)
|
||||
// or if if remote isn't a synthetic
|
||||
if (otherHypToken === remoteHypToken || tokensHasRouter(tokens, otherHypTokenAddress)) |
||||
continue; |
||||
|
||||
tokenRoutes[remoteCaip2Id][otherSynCaip2Id]?.push({ |
||||
type: RouteType.SyntheticToSynthetic, |
||||
...commonRouteProps, |
||||
originCaip2Id: remoteCaip2Id, |
||||
originRouterAddress: remoteRouterAddress, |
||||
originDecimals: remoteDecimals, |
||||
destCaip2Id: otherSynCaip2Id, |
||||
destRouterAddress: otherHypTokenAddress, |
||||
destDecimals: otherHypToken.decimals, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
return tokenRoutes; |
||||
} |
||||
|
||||
function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id[] { |
||||
const chains = new Set<ChainCaip2Id>(); |
||||
for (const token of tokens) { |
||||
chains.add(getChainIdFromToken(token.tokenCaip19Id)); |
||||
for (const hypToken of token.hypTokens) { |
||||
chains.add(getChainIdFromToken(hypToken.tokenCaip19Id)); |
||||
} |
||||
} |
||||
return Array.from(chains); |
||||
} |
||||
|
||||
function tokensHasRouter(tokens: TokenMetadataWithHypTokens[], router: Address) { |
||||
return !!tokens.find((t) => areAddressesEqual(t.routerAddress, router)); |
||||
} |
Loading…
Reference in new issue