Setup Jest unit testing

Create new CollateralToCollateral route type
Refactor route computation into separate file
Add unit tests for route computation
pull/51/head
J M Rossy 1 year ago
parent 0faea83ca8
commit 1014966351
  1. 3
      .eslintignore
  2. 29
      .github/workflows/ci.yml
  3. 16
      jest.config.js
  4. 3
      package.json
  5. 2
      src/features/tokens/SelectOrInputTokenIds.tsx
  6. 16
      src/features/tokens/adapters/AdapterFactory.ts
  7. 252
      src/features/tokens/routes/fetch.test.ts
  8. 166
      src/features/tokens/routes/fetch.ts
  9. 150
      src/features/tokens/routes/hooks.ts
  10. 5
      src/features/tokens/routes/types.ts
  11. 28
      src/features/tokens/routes/utils.ts
  12. 12
      src/features/transfer/useTokenTransfer.ts
  13. 2208
      yarn.lock

@ -4,4 +4,5 @@ build
coverage coverage
postcss.config.js postcss.config.js
next.config.js next.config.js
tailwind.config.js tailwind.config.js
jest.config.js

@ -19,7 +19,6 @@ jobs:
with: with:
path: .//node_modules path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: yarn-install - name: yarn-install
# Check out the lockfile from main, reinstall, and then # Check out the lockfile from main, reinstall, and then
# verify the lockfile matches what was committed. # verify the lockfile matches what was committed.
@ -37,19 +36,16 @@ jobs:
needs: [install] needs: [install]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: yarn-cache - name: yarn-cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: .//node_modules path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: build-cache - name: build-cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ./* path: ./*
key: ${{ github.sha }} key: ${{ github.sha }}
- name: build - name: build
run: yarn run build run: yarn run build
env: env:
@ -64,7 +60,6 @@ jobs:
with: with:
path: .//node_modules path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: prettier - name: prettier
run: | run: |
yarn run prettier yarn run prettier
@ -83,19 +78,17 @@ jobs:
with: with:
path: .//node_modules path: .//node_modules
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }} key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: lint - name: lint
run: yarn run lint run: yarn run lint
# test: test:
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: [build] needs: [build]
# steps: steps:
# - uses: actions/checkout@v2 - uses: actions/checkout@v2
# - uses: actions/cache@v2 - uses: actions/cache@v2
# with: with:
# path: ./* path: .//node_modules
# key: ${{ github.sha }} key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: test
# - name: test run: yarn run test
# run: yarn run test

@ -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)

@ -31,6 +31,7 @@
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1", "@trivago/prettier-plugin-sort-imports": "^4.1.1",
"@types/jest": "^29.5.3",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/react": "^18.2.7", "@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
@ -40,6 +41,7 @@
"eslint": "^8.41.0", "eslint": "^8.41.0",
"eslint-config-next": "^13.4.3", "eslint-config-next": "^13.4.3",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"jest": "^29.6.3",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
@ -62,6 +64,7 @@
"typecheck": "tsc", "typecheck": "tsc",
"lint": "next lint", "lint": "next lint",
"start": "next start", "start": "next start",
"test": "jest",
"prettier": "prettier --write ./src" "prettier": "prettier --write ./src"
}, },
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",

@ -24,7 +24,7 @@ export function SelectOrInputTokenIds({
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes);
let activeToken = '' as TokenCaip19Id; let activeToken = '' as TokenCaip19Id;
if (route?.type === RouteType.BaseToSynthetic) { if (route?.type === RouteType.CollateralToSynthetic) {
// If the origin is the base chain, use the collateralized token for balance checking // If the origin is the base chain, use the collateralized token for balance checking
activeToken = tokenCaip19Id; activeToken = tokenCaip19Id;
} else if (route) { } else if (route) {

@ -12,7 +12,13 @@ import {
parseCaip19Id, parseCaip19Id,
} from '../../caip/tokens'; } from '../../caip/tokens';
import { getMultiProvider, getProvider } from '../../multiProvider'; import { getMultiProvider, getProvider } from '../../multiProvider';
import { Route, RouteType } from '../routes/types'; import { Route } from '../routes/types';
import {
isRouteFromCollateral,
isRouteFromSynthetic,
isRouteToCollateral,
isRouteToSynthetic,
} from '../routes/utils';
import { import {
EvmHypCollateralAdapter, EvmHypCollateralAdapter,
@ -75,7 +81,7 @@ export class AdapterFactory {
static HypTokenAdapterFromRouteOrigin(route: Route) { static HypTokenAdapterFromRouteOrigin(route: Route) {
const { type, originCaip2Id, originRouterAddress, baseTokenCaip19Id } = route; const { type, originCaip2Id, originRouterAddress, baseTokenCaip19Id } = route;
if (type === RouteType.BaseToSynthetic) { if (isRouteFromCollateral(route)) {
return AdapterFactory.selectHypAdapter( return AdapterFactory.selectHypAdapter(
originCaip2Id, originCaip2Id,
originRouterAddress, originRouterAddress,
@ -83,7 +89,7 @@ export class AdapterFactory {
EvmHypCollateralAdapter, EvmHypCollateralAdapter,
isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter,
); );
} else if (type === RouteType.SyntheticToBase || type === RouteType.SyntheticToSynthetic) { } else if (isRouteFromSynthetic(route)) {
return AdapterFactory.selectHypAdapter( return AdapterFactory.selectHypAdapter(
originCaip2Id, originCaip2Id,
originRouterAddress, originRouterAddress,
@ -98,7 +104,7 @@ export class AdapterFactory {
static HypTokenAdapterFromRouteDest(route: Route) { static HypTokenAdapterFromRouteDest(route: Route) {
const { type, destCaip2Id, destRouterAddress, baseTokenCaip19Id } = route; const { type, destCaip2Id, destRouterAddress, baseTokenCaip19Id } = route;
if (type === RouteType.SyntheticToBase) { if (isRouteToCollateral(route)) {
return AdapterFactory.selectHypAdapter( return AdapterFactory.selectHypAdapter(
destCaip2Id, destCaip2Id,
destRouterAddress, destRouterAddress,
@ -106,7 +112,7 @@ export class AdapterFactory {
EvmHypCollateralAdapter, EvmHypCollateralAdapter,
isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, isNativeToken(baseTokenCaip19Id) ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter,
); );
} else if (type === RouteType.BaseToSynthetic || type === RouteType.SyntheticToSynthetic) { } else if (isRouteToSynthetic(route)) {
return AdapterFactory.selectHypAdapter( return AdapterFactory.selectHypAdapter(
destCaip2Id, destCaip2Id,
destRouterAddress, destRouterAddress,

@ -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));
}

@ -1,24 +1,13 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ProtocolType } from '@hyperlane-xyz/sdk';
import { areAddressesEqual, bytesToProtocolAddress } from '../../../utils/addresses';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { getCaip2Id } from '../../caip/chains'; import { getChainIdFromToken } from '../../caip/tokens';
import {
getCaip19Id,
getChainIdFromToken,
isNonFungibleToken,
parseCaip19Id,
resolveAssetNamespace,
} from '../../caip/tokens';
import { getMultiProvider } from '../../multiProvider';
import { AdapterFactory } from '../adapters/AdapterFactory';
import { getTokens, parseTokens } from '../metadata'; import { getTokens, parseTokens } from '../metadata';
import { TokenMetadata, TokenMetadataWithHypTokens } from '../types'; import { TokenMetadataWithHypTokens } from '../types';
import { RouteType, RoutesMap } from './types'; import { computeTokenRoutes, fetchRemoteHypTokens } from './fetch';
import { RoutesMap } from './types';
export function useTokenRoutes() { export function useTokenRoutes() {
const { const {
@ -44,137 +33,6 @@ export function useTokenRoutes() {
return { isLoading, error, tokenRoutes }; return { isLoading, error, tokenRoutes };
} }
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)
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 hypToken of token.hypTokens) {
const {
tokenCaip19Id: baseTokenCaip19Id,
routerAddress: baseRouterAddress,
decimals: baseDecimals,
} = token;
const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id);
const { chainCaip2Id: syntheticCaip2Id, address: syntheticRouterAddress } = parseCaip19Id(
hypToken.tokenCaip19Id,
);
const syntheticDecimals = hypToken.decimals;
const commonRouteProps = {
baseTokenCaip19Id,
baseRouterAddress,
};
tokenRoutes[baseChainCaip2Id][syntheticCaip2Id]?.push({
type: RouteType.BaseToSynthetic,
...commonRouteProps,
originCaip2Id: baseChainCaip2Id,
originRouterAddress: baseRouterAddress,
originDecimals: baseDecimals,
destCaip2Id: syntheticCaip2Id,
destRouterAddress: syntheticRouterAddress,
destDecimals: syntheticDecimals,
});
tokenRoutes[syntheticCaip2Id][baseChainCaip2Id]?.push({
type: RouteType.SyntheticToBase,
...commonRouteProps,
originCaip2Id: syntheticCaip2Id,
originRouterAddress: syntheticRouterAddress,
originDecimals: syntheticDecimals,
destCaip2Id: baseChainCaip2Id,
destRouterAddress: baseRouterAddress,
destDecimals: baseDecimals,
});
for (const otherHypToken of token.hypTokens) {
// Skip if it's same hypToken as parent loop (no route to self)
if (otherHypToken === hypToken) continue;
const { chainCaip2Id: otherSynCaip2Id, address: otherHypTokenAddress } = parseCaip19Id(
otherHypToken.tokenCaip19Id,
);
tokenRoutes[syntheticCaip2Id][otherSynCaip2Id]?.push({
type: RouteType.SyntheticToSynthetic,
...commonRouteProps,
originCaip2Id: syntheticCaip2Id,
originRouterAddress: syntheticRouterAddress,
originDecimals: syntheticDecimals,
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);
}
export function useRouteChains(tokenRoutes: RoutesMap): ChainCaip2Id[] { export function useRouteChains(tokenRoutes: RoutesMap): ChainCaip2Id[] {
return useMemo(() => { return useMemo(() => {
const allCaip2Ids = Object.keys(tokenRoutes) as ChainCaip2Id[]; const allCaip2Ids = Object.keys(tokenRoutes) as ChainCaip2Id[];

@ -1,7 +1,8 @@
export enum RouteType { export enum RouteType {
BaseToSynthetic = 'baseToSynthetic', CollateralToCollateral = 'collateralToCollateral',
CollateralToSynthetic = 'collateralToSynthetic',
SyntheticToSynthetic = 'syntheticToSynthetic', SyntheticToSynthetic = 'syntheticToSynthetic',
SyntheticToBase = 'syntheticToBase', SyntheticToCollateral = 'syntheticToCollateral',
} }
export interface Route { export interface Route {

@ -1,4 +1,4 @@
import { Route, RoutesMap } from './types'; import { Route, RouteType, RoutesMap } from './types';
export function getTokenRoutes( export function getTokenRoutes(
originCaip2Id: ChainCaip2Id, originCaip2Id: ChainCaip2Id,
@ -28,3 +28,29 @@ export function hasTokenRoute(
): boolean { ): boolean {
return !!getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); return !!getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes);
} }
export function isRouteToCollateral(route: Route) {
return (
route.type === RouteType.CollateralToCollateral ||
route.type === RouteType.SyntheticToCollateral
);
}
export function isRouteFromCollateral(route: Route) {
return (
route.type === RouteType.CollateralToCollateral ||
route.type === RouteType.CollateralToSynthetic
);
}
export function isRouteToSynthetic(route: Route) {
return (
route.type === RouteType.CollateralToSynthetic || route.type === RouteType.SyntheticToSynthetic
);
}
export function isRouteFromSynthetic(route: Route) {
return (
route.type === RouteType.SyntheticToCollateral || route.type === RouteType.SyntheticToSynthetic
);
}

@ -14,8 +14,8 @@ import { getMultiProvider } from '../multiProvider';
import { AppState, useStore } from '../store'; import { AppState, useStore } from '../store';
import { AdapterFactory } from '../tokens/adapters/AdapterFactory'; import { AdapterFactory } from '../tokens/adapters/AdapterFactory';
import { IHypTokenAdapter } from '../tokens/adapters/ITokenAdapter'; import { IHypTokenAdapter } from '../tokens/adapters/ITokenAdapter';
import { Route, RouteType, RoutesMap } from '../tokens/routes/types'; import { Route, RoutesMap } from '../tokens/routes/types';
import { getTokenRoute } from '../tokens/routes/utils'; import { getTokenRoute, isRouteFromCollateral, isRouteToCollateral } from '../tokens/routes/utils';
import { import {
AccountInfo, AccountInfo,
ActiveChainInfo, ActiveChainInfo,
@ -183,7 +183,7 @@ async function executeTransfer({
// it's possible that the collateral contract balance is insufficient to // it's possible that the collateral contract balance is insufficient to
// cover the remote transfer. This ensures the balance is sufficient or throws. // cover the remote transfer. This ensures the balance is sufficient or throws.
async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) { async function ensureSufficientCollateral(route: Route, weiAmount: string, isNft?: boolean) {
if (route.type !== RouteType.SyntheticToBase || isNft) return; if (!isRouteToCollateral(route) || isNft) return;
const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseTokenCaip19Id); const adapter = AdapterFactory.TokenAdapterFromAddress(route.baseTokenCaip19Id);
logger.debug('Checking collateral balance for token', route.baseTokenCaip19Id); logger.debug('Checking collateral balance for token', route.baseTokenCaip19Id);
const balance = await adapter.getBalance(route.baseRouterAddress); const balance = await adapter.getBalance(route.baseRouterAddress);
@ -215,7 +215,7 @@ async function executeEvmTransfer({
updateStatus, updateStatus,
sendTransaction, sendTransaction,
}: ExecuteTransferParams<providers.TransactionReceipt>) { }: ExecuteTransferParams<providers.TransactionReceipt>) {
const { type: routeType, baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute; const { baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute;
if (isTransferApproveRequired(tokenRoute, baseTokenCaip19Id)) { if (isTransferApproveRequired(tokenRoute, baseTokenCaip19Id)) {
updateStatus(TransferStatus.CreatingApprove); updateStatus(TransferStatus.CreatingApprove);
@ -244,7 +244,7 @@ async function executeEvmTransfer({
logger.debug('Quoted gas payment', gasPayment); logger.debug('Quoted gas payment', gasPayment);
// If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together
const txValue = const txValue =
routeType === RouteType.BaseToSynthetic && isNativeToken(baseTokenCaip19Id) isRouteFromCollateral(tokenRoute) && isNativeToken(baseTokenCaip19Id)
? BigNumber.from(gasPayment).add(weiAmountOrId) ? BigNumber.from(gasPayment).add(weiAmountOrId)
: gasPayment; : gasPayment;
const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({
@ -313,7 +313,7 @@ async function executeSealevelTransfer({
export function isTransferApproveRequired(route: Route, tokenCaip19Id: TokenCaip19Id) { export function isTransferApproveRequired(route: Route, tokenCaip19Id: TokenCaip19Id) {
return ( return (
!isNativeToken(tokenCaip19Id) && !isNativeToken(tokenCaip19Id) &&
route.type === RouteType.BaseToSynthetic && isRouteFromCollateral(route) &&
getProtocolType(route.originCaip2Id) === ProtocolType.Ethereum getProtocolType(route.originCaip2Id) === ProtocolType.Ethereum
); );
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save