Move config management to build-time (#120)
- Refactor chain, token, and route config management into build-time script - Combine chain, token, and route state into single `WarpContext` var - Fix outdated `publicRpcUrls` field in example chain configpull/127/head
parent
7ac7dcc127
commit
22fb63714b
@ -1,4 +1,4 @@ |
||||
import { IbcRoute, IbcToWarpRoute } from '../features/tokens/routes/types'; |
||||
import { IbcRoute, IbcToWarpRoute } from '../features/routes/types'; |
||||
|
||||
// Configs for manually-defined IBC-only routes
|
||||
export const ibcRoutes: Array<IbcRoute | IbcToWarpRoute> = []; |
||||
|
@ -0,0 +1,4 @@ |
||||
### About |
||||
|
||||
This folder contains pre-validated and processed configs for the the chains, tokens, and routes. |
||||
The contents are auto-generated by the `yarn build:configs` command. Changes will be overridden on new builds. |
@ -0,0 +1,33 @@ |
||||
import { ChainMap, ChainMetadata, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import type { RoutesMap } from '../features/routes/types'; |
||||
import type { TokenMetadata } from '../features/tokens/types'; |
||||
|
||||
import Chains from './_chains.json'; |
||||
import Routes from './_routes.json'; |
||||
import Tokens from './_tokens.json'; |
||||
|
||||
export interface WarpContext { |
||||
chains: ChainMap<ChainMetadata & { mailbox?: Address }>; |
||||
tokens: TokenMetadata[]; |
||||
routes: RoutesMap; |
||||
multiProvider: MultiProtocolProvider<{ mailbox?: Address }>; |
||||
} |
||||
|
||||
let warpContext: WarpContext; |
||||
|
||||
export function getWarpContext() { |
||||
if (!warpContext) { |
||||
warpContext = { |
||||
chains: Chains as any, |
||||
tokens: Tokens as any, |
||||
routes: Routes as any, |
||||
multiProvider: new MultiProtocolProvider<{ mailbox?: Address }>(Chains as any), |
||||
}; |
||||
} |
||||
return warpContext; |
||||
} |
||||
|
||||
export function setWarpContext(context: WarpContext) { |
||||
warpContext = context; |
||||
} |
@ -0,0 +1,19 @@ |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { getChainIdFromToken } from '../caip/tokens'; |
||||
import { getTokens } from '../tokens/metadata'; |
||||
|
||||
import { RoutesMap } from './types'; |
||||
|
||||
export function useRouteChains(tokenRoutes: RoutesMap): ChainCaip2Id[] { |
||||
return useMemo(() => { |
||||
const allCaip2Ids = Object.keys(tokenRoutes) as ChainCaip2Id[]; |
||||
const collateralCaip2Ids = getTokens().map((t) => getChainIdFromToken(t.tokenCaip19Id)); |
||||
return allCaip2Ids.sort((c1, c2) => { |
||||
// Surface collateral chains first
|
||||
if (collateralCaip2Ids.includes(c1) && !collateralCaip2Ids.includes(c2)) return -1; |
||||
else if (!collateralCaip2Ids.includes(c1) && collateralCaip2Ids.includes(c2)) return 1; |
||||
else return c1 > c2 ? 1 : -1; |
||||
}); |
||||
}, [tokenRoutes]); |
||||
} |
@ -1,61 +0,0 @@ |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import { useMemo } from 'react'; |
||||
|
||||
import { useToastError } from '../../../components/toast/useToastError'; |
||||
import { ibcRoutes } from '../../../consts/ibcRoutes'; |
||||
import { logger } from '../../../utils/logger'; |
||||
import { getChainIdFromToken } from '../../caip/tokens'; |
||||
import { getTokens, isIbcToken, parseTokens } from '../metadata'; |
||||
import { TokenMetadataWithHypTokens } from '../types'; |
||||
|
||||
import { computeTokenRoutes, fetchRemoteHypTokens } from './fetch'; |
||||
import { RoutesMap } from './types'; |
||||
import { mergeRoutes } from './utils'; |
||||
|
||||
export function useTokenRoutes() { |
||||
const { |
||||
isLoading, |
||||
data: tokenRoutes, |
||||
error, |
||||
} = useQuery( |
||||
['token-routes'], |
||||
async () => { |
||||
logger.info('Searching for token routes'); |
||||
const parsedTokens = await parseTokens(); |
||||
const tokens: TokenMetadataWithHypTokens[] = []; |
||||
for (const token of parsedTokens) { |
||||
// Skip querying of IBC tokens
|
||||
if (isIbcToken(token)) continue; |
||||
// Consider parallelizing here but concerned about RPC rate limits
|
||||
const tokenWithHypTokens = await fetchRemoteHypTokens(token, parsedTokens); |
||||
tokens.push(tokenWithHypTokens); |
||||
} |
||||
let routes = computeTokenRoutes(tokens); |
||||
|
||||
if (ibcRoutes) { |
||||
logger.info('Found ibc route configs, adding to route map'); |
||||
routes = mergeRoutes(routes, ibcRoutes); |
||||
} |
||||
|
||||
return routes; |
||||
}, |
||||
{ retry: false }, |
||||
); |
||||
|
||||
useToastError(error, 'Error fetching token routes'); |
||||
|
||||
return { isLoading, error, tokenRoutes }; |
||||
} |
||||
|
||||
export function useRouteChains(tokenRoutes: RoutesMap): ChainCaip2Id[] { |
||||
return useMemo(() => { |
||||
const allCaip2Ids = Object.keys(tokenRoutes) as ChainCaip2Id[]; |
||||
const collateralCaip2Ids = getTokens().map((t) => getChainIdFromToken(t.tokenCaip19Id)); |
||||
return allCaip2Ids.sort((c1, c2) => { |
||||
// Surface collateral chains first
|
||||
if (collateralCaip2Ids.includes(c1) && !collateralCaip2Ids.includes(c2)) return -1; |
||||
else if (!collateralCaip2Ids.includes(c1) && collateralCaip2Ids.includes(c2)) return 1; |
||||
else return c1 > c2 ? 1 : -1; |
||||
}); |
||||
}, [tokenRoutes]); |
||||
} |
@ -0,0 +1,9 @@ |
||||
#!/bin/bash |
||||
|
||||
# Create placeholder files or ts-node script will fail |
||||
echo "{}" > ./src/context/_chains.json |
||||
echo "[]" > ./src/context/_tokens.json |
||||
echo "{}" > ./src/context/_routes.json |
||||
|
||||
# Run actual build script |
||||
yarn ts-node src/scripts/buildConfigs/index.ts |
@ -0,0 +1,33 @@ |
||||
import path from 'path'; |
||||
import { z } from 'zod'; |
||||
|
||||
import { ChainMap, ChainMetadata, ChainMetadataSchema, chainMetadata } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import ChainsJson from '../../consts/chains.json'; |
||||
import { chains as ChainsTS } from '../../consts/chains.ts'; |
||||
import { cosmosDefaultChain } from '../../features/chains/cosmosDefault'; |
||||
import { logger } from '../../utils/logger'; |
||||
|
||||
import { readYaml } from './utils'; |
||||
|
||||
export const ChainConfigSchema = z.record( |
||||
ChainMetadataSchema.and(z.object({ mailbox: z.string().optional() })), |
||||
); |
||||
|
||||
export function getProcessedChainConfigs() { |
||||
const ChainsYaml = readYaml(path.resolve(__dirname, '../../consts/chains.yaml')); |
||||
|
||||
// Chains must include a cosmos chain or CosmosKit throws errors
|
||||
const result = ChainConfigSchema.safeParse({ |
||||
cosmoshub: cosmosDefaultChain, |
||||
...ChainsJson, |
||||
...ChainsYaml, |
||||
...ChainsTS, |
||||
}); |
||||
if (!result.success) { |
||||
logger.warn('Invalid chain config', result.error); |
||||
throw new Error(`Invalid chain config: ${result.error.toString()}`); |
||||
} |
||||
const customChainConfigs = result.data as ChainMap<ChainMetadata & { mailbox?: Address }>; |
||||
return { ...chainMetadata, ...customChainConfigs }; |
||||
} |
@ -0,0 +1,46 @@ |
||||
#!/usr/bin/env node |
||||
import fs from 'fs'; |
||||
import path from 'path'; |
||||
|
||||
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { type WarpContext, setWarpContext } from '../../context/context'; |
||||
import { logger } from '../../utils/logger'; |
||||
|
||||
import { getProcessedChainConfigs } from './chains'; |
||||
import { getRouteConfigs } from './routes'; |
||||
import { getProcessedTokenConfigs } from './tokens'; |
||||
|
||||
const CHAINS_OUT_PATH = path.resolve(__dirname, '../../context/_chains.json'); |
||||
const TOKENS_OUT_PATH = path.resolve(__dirname, '../../context/_tokens.json'); |
||||
const ROUTES_OUT_PATH = path.resolve(__dirname, '../../context/_routes.json'); |
||||
|
||||
async function main() { |
||||
logger.info('Getting chains'); |
||||
const chains = getProcessedChainConfigs(); |
||||
const multiProvider = new MultiProtocolProvider<{ mailbox?: Address }>(chains); |
||||
logger.info('Getting tokens'); |
||||
const tokens = await getProcessedTokenConfigs(multiProvider); |
||||
|
||||
const context: WarpContext = { |
||||
chains, |
||||
tokens, |
||||
routes: {}, |
||||
multiProvider, |
||||
}; |
||||
setWarpContext(context); |
||||
|
||||
logger.info('Getting routes'); |
||||
const routes = await getRouteConfigs(context); |
||||
|
||||
logger.info(`Writing chains to file ${CHAINS_OUT_PATH}`); |
||||
fs.writeFileSync(CHAINS_OUT_PATH, JSON.stringify(chains, null, 2), 'utf8'); |
||||
logger.info(`Writing tokens to file ${TOKENS_OUT_PATH}`); |
||||
fs.writeFileSync(TOKENS_OUT_PATH, JSON.stringify(tokens, null, 2), 'utf8'); |
||||
logger.info(`Writing routes to file ${ROUTES_OUT_PATH}`); |
||||
fs.writeFileSync(ROUTES_OUT_PATH, JSON.stringify(routes, null, 2), 'utf8'); |
||||
} |
||||
|
||||
main() |
||||
.then(() => logger.info('Done processing configs')) |
||||
.catch((error) => logger.warn('Error processing configs', error)); |
@ -1,8 +1,8 @@ |
||||
import { TokenType } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { SOL_ZERO_ADDRESS } from '../../../consts/values'; |
||||
import { SOL_ZERO_ADDRESS } from '../../consts/values'; |
||||
|
||||
import { computeTokenRoutes } from './fetch'; |
||||
import { computeTokenRoutes } from './routes'; |
||||
|
||||
describe('computeTokenRoutes', () => { |
||||
it('Handles empty list', () => { |
@ -0,0 +1,132 @@ |
||||
import path from 'path'; |
||||
|
||||
import { |
||||
EvmTokenAdapter, |
||||
ITokenAdapter, |
||||
MultiProtocolProvider, |
||||
TokenType, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { ProtocolType } from '@hyperlane-xyz/utils'; |
||||
|
||||
import TokensJson from '../../consts/tokens.json'; |
||||
import { tokenList as TokensTS } from '../../consts/tokens.ts'; |
||||
import { getCaip2Id } from '../../features/caip/chains'; |
||||
import { |
||||
getCaip19Id, |
||||
getNativeTokenAddress, |
||||
resolveAssetNamespace, |
||||
} from '../../features/caip/tokens'; |
||||
import { getHypErc20CollateralContract } from '../../features/tokens/contracts/evmContracts'; |
||||
import { |
||||
MinimalTokenMetadata, |
||||
TokenMetadata, |
||||
WarpTokenConfig, |
||||
WarpTokenConfigSchema, |
||||
} from '../../features/tokens/types'; |
||||
import { logger } from '../../utils/logger'; |
||||
|
||||
import { readYaml } from './utils'; |
||||
|
||||
export async function getProcessedTokenConfigs(multiProvider: MultiProtocolProvider) { |
||||
const TokensYaml = readYaml(path.resolve(__dirname, '../../consts/tokens.yaml')); |
||||
const tokenList = [...TokensJson, ...TokensYaml, ...TokensTS]; |
||||
const tokens = await parseTokenConfigs(multiProvider, tokenList); |
||||
return tokens; |
||||
} |
||||
|
||||
// Converts the more user-friendly config format into a validated, extended format
|
||||
// that's easier for the UI to work with
|
||||
async function parseTokenConfigs( |
||||
multiProvider: MultiProtocolProvider, |
||||
configList: WarpTokenConfig, |
||||
): Promise<TokenMetadata[]> { |
||||
const result = WarpTokenConfigSchema.safeParse(configList); |
||||
if (!result.success) { |
||||
logger.warn('Invalid token config', result.error); |
||||
throw new Error(`Invalid token config: ${result.error.toString()}`); |
||||
} |
||||
|
||||
const parsedConfig = result.data; |
||||
const tokenMetadata: TokenMetadata[] = []; |
||||
for (const config of parsedConfig) { |
||||
const { type, chainId, logoURI, igpTokenAddress } = config; |
||||
|
||||
const protocol = multiProvider.getChainMetadata(chainId).protocol || ProtocolType.Ethereum; |
||||
const chainCaip2Id = getCaip2Id(protocol, chainId); |
||||
const isNative = type == TokenType.native; |
||||
const isNft = type === TokenType.collateral && config.isNft; |
||||
const isSpl2022 = type === TokenType.collateral && config.isSpl2022; |
||||
const address = |
||||
type === TokenType.collateral ? config.address : getNativeTokenAddress(protocol); |
||||
const routerAddress = |
||||
type === TokenType.collateral |
||||
? config.hypCollateralAddress |
||||
: type === TokenType.native |
||||
? config.hypNativeAddress |
||||
: ''; |
||||
const namespace = resolveAssetNamespace(protocol, isNative, isNft, isSpl2022); |
||||
const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, address); |
||||
|
||||
const { name, symbol, decimals } = await fetchNameAndDecimals( |
||||
multiProvider, |
||||
config, |
||||
protocol, |
||||
routerAddress, |
||||
isNft, |
||||
); |
||||
|
||||
tokenMetadata.push({ |
||||
name, |
||||
symbol, |
||||
decimals, |
||||
logoURI, |
||||
type, |
||||
tokenCaip19Id, |
||||
routerAddress, |
||||
igpTokenAddress, |
||||
}); |
||||
} |
||||
return tokenMetadata; |
||||
} |
||||
|
||||
async function fetchNameAndDecimals( |
||||
multiProvider: MultiProtocolProvider, |
||||
tokenConfig: WarpTokenConfig[number], |
||||
protocol: ProtocolType, |
||||
routerAddress: Address, |
||||
isNft?: boolean, |
||||
): Promise<MinimalTokenMetadata> { |
||||
const { type, chainId, name, symbol, decimals } = tokenConfig; |
||||
if (name && symbol && decimals) { |
||||
// Already provided in the config
|
||||
return { name, symbol, decimals }; |
||||
} |
||||
|
||||
const chainMetadata = multiProvider.getChainMetadata(chainId); |
||||
|
||||
if (type === TokenType.native) { |
||||
// Use the native token config that may be in the chain metadata
|
||||
const tokenMetadata = chainMetadata.nativeToken; |
||||
if (!tokenMetadata) throw new Error('Name, symbol, or decimals is missing for native token'); |
||||
return tokenMetadata; |
||||
} |
||||
|
||||
if (type === TokenType.collateral) { |
||||
// Fetch the data from the contract
|
||||
let tokenAdapter: ITokenAdapter; |
||||
if (protocol === ProtocolType.Ethereum) { |
||||
const provider = multiProvider.getEthersV5Provider(chainId); |
||||
const collateralContract = getHypErc20CollateralContract(routerAddress, provider); |
||||
const wrappedTokenAddr = await collateralContract.wrappedToken(); |
||||
tokenAdapter = new EvmTokenAdapter(chainMetadata.name, multiProvider, { |
||||
token: wrappedTokenAddr, |
||||
}); |
||||
} else { |
||||
// TODO solana support when hyp tokens have metadata
|
||||
throw new Error('Name, symbol, and decimals is required for non-EVM token configs'); |
||||
} |
||||
return tokenAdapter.getMetadata(isNft); |
||||
} |
||||
|
||||
throw new Error(`Unsupported token type ${type}`); |
||||
} |
@ -0,0 +1,6 @@ |
||||
import fs from 'fs'; |
||||
import { parse } from 'yaml'; |
||||
|
||||
export function readYaml(path: string) { |
||||
return parse(fs.readFileSync(path, 'utf-8')); |
||||
} |
Loading…
Reference in new issue