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 config
pull/127/head
J M Rossy 9 months ago committed by GitHub
parent 7ac7dcc127
commit 22fb63714b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .gitignore
  2. 7
      package.json
  3. 6
      src/consts/chains.ts
  4. 2
      src/consts/ibcRoutes.ts
  5. 4
      src/context/README.md
  6. 33
      src/context/context.ts
  7. 48
      src/features/chains/metadata.ts
  8. 11
      src/features/multiProvider.ts
  9. 19
      src/features/routes/hooks.ts
  10. 0
      src/features/routes/types.ts
  11. 14
      src/features/routes/utils.ts
  12. 8
      src/features/tokens/AdapterFactory.ts
  13. 4
      src/features/tokens/SelectOrInputTokenIds.tsx
  14. 4
      src/features/tokens/TokenListModal.tsx
  15. 4
      src/features/tokens/TokenSelectField.tsx
  16. 4
      src/features/tokens/approval.ts
  17. 4
      src/features/tokens/balances.ts
  18. 126
      src/features/tokens/metadata.ts
  19. 61
      src/features/tokens/routes/hooks.ts
  20. 22
      src/features/transfer/TransferTokenCard.tsx
  21. 6
      src/features/transfer/TransferTokenForm.tsx
  22. 2
      src/features/transfer/types.ts
  23. 10
      src/features/transfer/useIgpQuote.ts
  24. 4
      src/features/transfer/useTokenTransfer.ts
  25. 4
      src/features/transfer/utils.ts
  26. 4
      src/features/transfer/validateForm.ts
  27. 9
      src/scripts/buildConfigs/build.sh
  28. 33
      src/scripts/buildConfigs/chains.ts
  29. 46
      src/scripts/buildConfigs/index.ts
  30. 4
      src/scripts/buildConfigs/routes.test.ts
  31. 58
      src/scripts/buildConfigs/routes.ts
  32. 132
      src/scripts/buildConfigs/tokens.ts
  33. 6
      src/scripts/buildConfigs/utils.ts
  34. 3
      tsconfig.json

3
.gitignore vendored

@ -15,8 +15,7 @@ coverage.json
/out/
# production
/src/types/
/src/deploy/output
/src/context/*.json
/artifacts
/build
/dist

@ -78,12 +78,13 @@
},
"scripts": {
"clean": "rm -rf dist cache .next",
"dev": "next dev",
"build": "next build",
"dev": "yarn build:configs && next dev",
"build": "yarn build:configs && next build",
"build:configs": "./src/scripts/buildConfigs/build.sh",
"typecheck": "tsc",
"lint": "next lint",
"start": "next start",
"test": "jest",
"test": "yarn build:configs && jest",
"prettier": "prettier --write ./src"
},
"types": "dist/src/index.d.ts",

@ -7,12 +7,12 @@ import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
export const chains: ChainMap<ChainMetadata & { mailbox?: Address }> = {
// mycustomchain: {
// protocol: ProtocolType.Ethereum,
// chainId: 1234,
// domainId: 1234,
// chainId: 123123,
// domainId: 123123,
// name: 'mycustomchain',
// displayName: 'My Chain',
// nativeToken: { name: 'Ether', symbol: 'ETH', decimals: 18 },
// publicRpcUrls: [{ http: 'https://mycustomchain-rpc.com' }],
// rpcUrls: [{ http: 'https://mycustomchain-rpc.com' }],
// blockExplorers: [
// {
// name: 'MyCustomScan',

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

@ -1,59 +1,21 @@
import type { AssetList, Chain as CosmosChain } from '@chain-registry/types';
import type { Chain as WagmiChain } from '@wagmi/core';
import { z } from 'zod';
import {
ChainMap,
ChainMetadata,
ChainMetadataSchema,
ChainName,
chainMetadata,
chainMetadataToWagmiChain,
} from '@hyperlane-xyz/sdk';
import { ChainName, chainMetadataToWagmiChain } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { chains as ChainsTS } from '../../consts/chains';
import ChainsJson from '../../consts/chains.json';
import ChainsYaml from '../../consts/chains.yaml';
import { logger } from '../../utils/logger';
import { cosmosDefaultChain } from './cosmosDefault';
let chainConfigs: ChainMap<ChainMetadata & { mailbox?: Address }>;
export const ChainConfigSchema = z.record(
ChainMetadataSchema.and(z.object({ mailbox: z.string().optional() })),
);
export function getChainConfigs() {
if (!chainConfigs) {
// Chains must include a cosmos chain or CosmosKit throws errors
const result = ChainConfigSchema.safeParse({
cosmoshub: cosmosDefaultChain,
...ChainsJson,
...ChainsYaml,
...ChainsTS,
});
if (!result.success) {
logger.error('Invalid chain config', result.error);
throw new Error(`Invalid chain config: ${result.error.toString()}`);
}
const customChainConfigs = result.data as ChainMap<ChainMetadata & { mailbox?: Address }>;
chainConfigs = { ...chainMetadata, ...customChainConfigs };
}
return chainConfigs;
}
import { getWarpContext } from '../../context/context';
// Metadata formatted for use in Wagmi config
export function getWagmiChainConfig(): WagmiChain[] {
const evmChains = Object.values(getChainConfigs()).filter(
const evmChains = Object.values(getWarpContext().chains).filter(
(c) => !c.protocol || c.protocol === ProtocolType.Ethereum,
);
return evmChains.map(chainMetadataToWagmiChain);
}
export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList[] } {
const cosmosChains = Object.values(getChainConfigs()).filter(
const cosmosChains = Object.values(getWarpContext().chains).filter(
(c) => c.protocol === ProtocolType.Cosmos,
);
const chains = cosmosChains.map((c) => ({
@ -143,7 +105,7 @@ export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList
}
export function getCosmosChainNames(): ChainName[] {
return Object.values(getChainConfigs())
return Object.values(getWarpContext().chains)
.filter((c) => c.protocol === ProtocolType.Cosmos)
.map((c) => c.name);
}

@ -1,16 +1,11 @@
import { MultiProtocolProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { parseCaip2Id } from './caip/chains';
import { getChainConfigs } from './chains/metadata';
import { getWarpContext } from '../context/context';
let multiProvider: MultiProtocolProvider<{ mailbox?: Address }>;
import { parseCaip2Id } from './caip/chains';
export function getMultiProvider() {
if (!multiProvider) {
multiProvider = new MultiProtocolProvider<{ mailbox?: Address }>(getChainConfigs());
}
return multiProvider;
return getWarpContext().multiProvider;
}
export function getEvmProvider(id: ChainCaip2Id) {

@ -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,6 +1,4 @@
import { deepCopy } from '@hyperlane-xyz/utils';
import { isNativeToken } from '../../caip/tokens';
import { isNativeToken } from '../caip/tokens';
import { IbcRoute, IbcToWarpRoute, Route, RouteType, RoutesMap, WarpRoute } from './types';
@ -83,13 +81,3 @@ export function isIbcOnlyRoute(route: Route): route is IbcRoute {
export function isIbcToWarpRoute(route: Route): route is IbcToWarpRoute {
return route.type === RouteType.IbcNativeToHypSynthetic;
}
export function mergeRoutes(routes: RoutesMap, newRoutes: Route[]) {
const mergedRoutes = deepCopy(routes);
for (const route of newRoutes) {
mergedRoutes[route.originCaip2Id] ||= {};
mergedRoutes[route.originCaip2Id][route.destCaip2Id] ||= [];
mergedRoutes[route.originCaip2Id][route.destCaip2Id].push(route);
}
return mergedRoutes;
}

@ -24,9 +24,7 @@ import { Address, ProtocolType, convertToProtocolAddress } from '@hyperlane-xyz/
import { parseCaip2Id } from '../caip/chains';
import { AssetNamespace, getChainIdFromToken, isNativeToken, parseCaip19Id } from '../caip/tokens';
import { getMultiProvider } from '../multiProvider';
import { getToken } from './metadata';
import { Route } from './routes/types';
import { Route } from '../routes/types';
import {
isIbcRoute,
isIbcToWarpRoute,
@ -35,7 +33,9 @@ import {
isRouteToCollateral,
isRouteToSynthetic,
isWarpRoute,
} from './routes/utils';
} from '../routes/utils';
import { getToken } from './metadata';
export class AdapterFactory {
static NativeAdapterFromChain(

@ -2,13 +2,13 @@ import { useFormikContext } from 'formik';
import { TextField } from '../../components/input/TextField';
import { AssetNamespace, getCaip19Id } from '../caip/tokens';
import { RouteType, RoutesMap } from '../routes/types';
import { getTokenRoute, isWarpRoute } from '../routes/utils';
import { TransferFormValues } from '../transfer/types';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';
import { SelectTokenIdField } from './SelectTokenIdField';
import { useContractSupportsTokenByOwner, useIsSenderNftOwner } from './balances';
import { RouteType, RoutesMap } from './routes/types';
import { getTokenRoute, isWarpRoute } from './routes/utils';
export function SelectOrInputTokenIds({
disabled,

@ -8,10 +8,10 @@ import { config } from '../../consts/config';
import InfoIcon from '../../images/icons/info-circle.svg';
import { getAssetNamespace, getTokenAddress, isNativeToken } from '../caip/tokens';
import { getChainDisplayName } from '../chains/utils';
import { RoutesMap } from '../routes/types';
import { hasTokenRoute } from '../routes/utils';
import { getTokens } from './metadata';
import { RoutesMap } from './routes/types';
import { hasTokenRoute } from './routes/utils';
import { TokenMetadata } from './types';
export function TokenListModal({

@ -5,12 +5,12 @@ import { useEffect, useState } from 'react';
import { TokenIcon } from '../../components/icons/TokenIcon';
import ChevronIcon from '../../images/icons/chevron-down.svg';
import { isNonFungibleToken } from '../caip/tokens';
import { RoutesMap } from '../routes/types';
import { getTokenRoutes } from '../routes/utils';
import { TransferFormValues } from '../transfer/types';
import { TokenListModal } from './TokenListModal';
import { getToken } from './metadata';
import { RoutesMap } from './routes/types';
import { getTokenRoutes } from './routes/utils';
import { TokenMetadata } from './types';
type Props = {

@ -7,11 +7,11 @@ import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { getTokenAddress, isNativeToken, isNonFungibleToken } from '../caip/tokens';
import { getEvmProvider } from '../multiProvider';
import { Route } from '../routes/types';
import { isRouteFromCollateral, isWarpRoute } from '../routes/utils';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';
import { getErc20Contract, getErc721Contract } from './contracts/evmContracts';
import { Route } from './routes/types';
import { isRouteFromCollateral, isWarpRoute } from './routes/utils';
export function useIsApproveRequired(
tokenCaip19Id: TokenCaip19Id,

@ -8,14 +8,14 @@ import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens';
import { getEvmProvider } from '../multiProvider';
import { RoutesMap } from '../routes/types';
import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isRouteFromNative } from '../routes/utils';
import { useStore } from '../store';
import { TransferFormValues } from '../transfer/types';
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol';
import { AdapterFactory } from './AdapterFactory';
import { getHypErc721Contract } from './contracts/evmContracts';
import { RoutesMap } from './routes/types';
import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isRouteFromNative } from './routes/utils';
export function useOriginBalance(
{ originCaip2Id, destinationCaip2Id, tokenCaip19Id }: TransferFormValues,

@ -1,27 +1,9 @@
import { EvmTokenAdapter, ITokenAdapter, TokenType } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { getWarpContext } from '../../context/context';
import { tokenList as TokensTS } from '../../consts/tokens';
import TokensJson from '../../consts/tokens.json';
import TokensYaml from '../../consts/tokens.yaml';
import { logger } from '../../utils/logger';
import { getCaip2Id } from '../caip/chains';
import { getCaip19Id, getNativeTokenAddress, resolveAssetNamespace } from '../caip/tokens';
import { getMultiProvider } from '../multiProvider';
import { getHypErc20CollateralContract } from './contracts/evmContracts';
import {
IbcTokenTypes,
MinimalTokenMetadata,
TokenMetadata,
WarpTokenConfig,
WarpTokenConfigSchema,
} from './types';
let tokens: TokenMetadata[];
import { IbcTokenTypes, TokenMetadata } from './types';
export function getTokens() {
return tokens || [];
return getWarpContext()?.tokens || [];
}
export function getToken(tokenCaip19Id: TokenCaip19Id) {
@ -32,108 +14,6 @@ export function findTokensByAddress(address: Address) {
return getTokens().filter((t) => t.tokenCaip19Id.includes(address));
}
export async function parseTokens() {
if (!tokens) {
const tokenList = [...TokensJson, ...TokensYaml, ...TokensTS];
tokens = await parseTokenConfigs(tokenList);
}
return tokens;
}
export function isIbcToken(token: TokenMetadata) {
return Object.values(IbcTokenTypes).includes(token.type as IbcTokenTypes);
}
// Converts the more user-friendly config format into a validated, extended format
// that's easier for the UI to work with
async function parseTokenConfigs(configList: WarpTokenConfig): Promise<TokenMetadata[]> {
const result = WarpTokenConfigSchema.safeParse(configList);
if (!result.success) {
logger.error('Invalid token config', result.error);
throw new Error(`Invalid token config: ${result.error.toString()}`);
}
const multiProvider = getMultiProvider();
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(
config,
protocol,
routerAddress,
isNft,
);
tokenMetadata.push({
name,
symbol,
decimals,
logoURI,
type,
tokenCaip19Id,
routerAddress,
igpTokenAddress,
});
}
return tokenMetadata;
}
async function fetchNameAndDecimals(
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 multiProvider = getMultiProvider();
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}`);
}

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

@ -1,15 +1,12 @@
import { WideChevron } from '@hyperlane-xyz/widgets';
import { Spinner } from '../../components/animation/Spinner';
import { Card } from '../../components/layout/Card';
import { getWarpContext } from '../../context/context';
import { Color } from '../../styles/Color';
import { useTokenRoutes } from '../tokens/routes/hooks';
import { TransferTokenForm } from './TransferTokenForm';
export function TransferTokenCard() {
const { tokenRoutes, isLoading, error: routesError } = useTokenRoutes();
return (
<Card className="w-100 sm:w-[31rem]">
<>
@ -22,22 +19,7 @@ export function TransferTokenCard() {
color={Color.primaryBlue}
/>
</div>
{tokenRoutes && <TransferTokenForm tokenRoutes={tokenRoutes} />}
{isLoading && (
<div className="my-24 flex flex-col items-center">
<Spinner />
<h3 className="mt-5 text-sm text-gray-500">Finding token routes</h3>
</div>
)}
{routesError && (
<div className="my-32 flex flex-col items-center text-center">
<h3 className="text-red-500">Error searching for token routes.</h3>
<div className="mt-3 text-sm text-red-500">
Please ensure token configs are valid and RPCs are healthy.
</div>
<div className="mt-4 text-xs text-gray-500">{routesError.toString()}</div>
</div>
)}
<TransferTokenForm tokenRoutes={getWarpContext().routes} />
</>
</Card>
);

@ -18,15 +18,15 @@ import { logger } from '../../utils/logger';
import { getTokenAddress, isNonFungibleToken } from '../caip/tokens';
import { ChainSelectField } from '../chains/ChainSelectField';
import { getChainDisplayName } from '../chains/utils';
import { useRouteChains } from '../routes/hooks';
import { RoutesMap, WarpRoute } from '../routes/types';
import { getTokenRoute, isIbcOnlyRoute } from '../routes/utils';
import { useStore } from '../store';
import { SelectOrInputTokenIds } from '../tokens/SelectOrInputTokenIds';
import { TokenSelectField } from '../tokens/TokenSelectField';
import { useIsApproveRequired } from '../tokens/approval';
import { useDestinationBalance, useOriginBalance } from '../tokens/balances';
import { getToken } from '../tokens/metadata';
import { useRouteChains } from '../tokens/routes/hooks';
import { RoutesMap, WarpRoute } from '../tokens/routes/types';
import { getTokenRoute, isIbcOnlyRoute } from '../tokens/routes/utils';
import { useAccountAddressForChain, useAccounts } from '../wallet/hooks/multiProtocol';
import { TransferFormValues } from './types';

@ -1,4 +1,4 @@
import type { Route } from '../tokens/routes/types';
import type { Route } from '../routes/types';
export interface TransferFormValues {
originCaip2Id: ChainCaip2Id;

@ -9,16 +9,16 @@ import { COSM_IGP_QUOTE, SOL_IGP_QUOTE } from '../../consts/values';
import { getChainReference, getProtocolType } from '../caip/chains';
import { AssetNamespace, getCaip19Id, getNativeTokenAddress } from '../caip/tokens';
import { getChainMetadata, getMultiProvider } from '../multiProvider';
import { useStore } from '../store';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { findTokensByAddress, getToken } from '../tokens/metadata';
import { Route } from '../tokens/routes/types';
import { Route } from '../routes/types';
import {
isIbcOnlyRoute,
isIbcToWarpRoute,
isRouteFromCollateral,
isRouteFromNative,
} from '../tokens/routes/utils';
} from '../routes/utils';
import { useStore } from '../store';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { findTokensByAddress, getToken } from '../tokens/metadata';
import { IgpQuote, IgpTokenType } from './types';

@ -22,11 +22,11 @@ import { logger } from '../../utils/logger';
import { parseCaip2Id } from '../caip/chains';
import { isNonFungibleToken } from '../caip/tokens';
import { getChainMetadata, getMultiProvider } from '../multiProvider';
import { Route, RoutesMap } from '../routes/types';
import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isWarpRoute } from '../routes/utils';
import { AppState, useStore } from '../store';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { isApproveRequired } from '../tokens/approval';
import { Route, RoutesMap } from '../tokens/routes/types';
import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isWarpRoute } from '../tokens/routes/utils';
import {
getAccountAddressForChain,
useAccounts,

@ -8,9 +8,9 @@ import { ProtocolType, convertDecimals } from '@hyperlane-xyz/utils';
import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { isNonFungibleToken } from '../caip/tokens';
import { Route } from '../routes/types';
import { isRouteToCollateral, isWarpRoute } from '../routes/utils';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { Route } from '../tokens/routes/types';
import { isRouteToCollateral, isWarpRoute } from '../tokens/routes/utils';
// In certain cases, like when a synthetic token has >1 collateral tokens
// it's possible that the collateral contract balance is insufficient to

@ -15,11 +15,11 @@ import { logger } from '../../utils/logger';
import { getProtocolType } from '../caip/chains';
import { isNonFungibleToken, parseCaip19Id } from '../caip/tokens';
import { getChainMetadata } from '../multiProvider';
import { Route, RoutesMap } from '../routes/types';
import { getTokenRoute, isIbcOnlyRoute } from '../routes/utils';
import { AppState } from '../store';
import { AdapterFactory } from '../tokens/AdapterFactory';
import { getToken } from '../tokens/metadata';
import { Route, RoutesMap } from '../tokens/routes/types';
import { getTokenRoute, isIbcOnlyRoute } from '../tokens/routes/utils';
import { getAccountAddressForChain } from '../wallet/hooks/multiProtocol';
import { AccountInfo } from '../wallet/hooks/types';

@ -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', () => {

@ -1,17 +1,38 @@
import { ProtocolType, bytesToProtocolAddress, eqAddress } from '@hyperlane-xyz/utils';
import { ProtocolType, bytesToProtocolAddress, deepCopy, eqAddress } from '@hyperlane-xyz/utils';
import { ibcRoutes } from '../../consts/ibcRoutes';
import { WarpContext } from '../../context/context';
import { getCaip2Id } from '../../features/caip/chains';
import { getChainIdFromToken, isNonFungibleToken } from '../../features/caip/tokens';
import { Route, RouteType, RoutesMap } from '../../features/routes/types';
import { AdapterFactory } from '../../features/tokens/AdapterFactory';
import { isIbcToken } from '../../features/tokens/metadata';
import { TokenMetadata, TokenMetadataWithHypTokens } from '../../features/tokens/types';
import { logger } from '../../utils/logger';
export async function getRouteConfigs(context: WarpContext): Promise<RoutesMap> {
logger.info('Searching for token routes');
const processedTokens: TokenMetadataWithHypTokens[] = [];
for (const token of context.tokens) {
// Skip querying of IBC tokens
if (isIbcToken(token)) continue;
const tokenWithHypTokens = await fetchRemoteHypTokens(context, token);
processedTokens.push(tokenWithHypTokens);
}
let routes = computeTokenRoutes(processedTokens);
import { logger } from '../../../utils/logger';
import { getCaip2Id } from '../../caip/chains';
import { getChainIdFromToken, isNonFungibleToken } from '../../caip/tokens';
import { getMultiProvider } from '../../multiProvider';
import { AdapterFactory } from '../AdapterFactory';
import { TokenMetadata, TokenMetadataWithHypTokens } from '../types';
if (ibcRoutes) {
logger.info('Found ibc route configs, adding to route map');
routes = mergeRoutes(routes, ibcRoutes);
}
import { RouteType, RoutesMap } from './types';
logger.info('Done searching for token routes');
return routes;
}
export async function fetchRemoteHypTokens(
context: WarpContext,
baseToken: TokenMetadata,
allTokens: TokenMetadata[],
): Promise<TokenMetadataWithHypTokens> {
const {
symbol: baseSymbol,
@ -24,18 +45,17 @@ export async function fetchRemoteHypTokens(
const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter);
const remoteRouters = await baseAdapter.getAllRouters();
logger.info(`Router addresses found:`, remoteRouters);
logger.info(`Router addresses found:`, remoteRouters.length);
const multiProvider = getMultiProvider();
const hypTokens = await Promise.all(
remoteRouters.map(async (router) => {
const destMetadata = multiProvider.getChainMetadata(router.domain);
const destMetadata = context.multiProvider.getChainMetadata(router.domain);
const protocol = destMetadata.protocol || ProtocolType.Ethereum;
const chain = getCaip2Id(protocol, multiProvider.getChainId(router.domain));
const chain = getCaip2Id(protocol, context.multiProvider.getChainId(router.domain));
const formattedAddress = bytesToProtocolAddress(router.address, protocol);
if (isNft) return { chain, router: formattedAddress, decimals: 0 };
// Attempt to find the decimals from the token list
const routerMetadata = allTokens.find((token) =>
const routerMetadata = context.tokens.find((token) =>
eqAddress(formattedAddress, token.routerAddress),
);
if (routerMetadata)
@ -156,3 +176,13 @@ function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id
function findTokenByRouter(tokens: TokenMetadataWithHypTokens[], router: Address) {
return tokens.find((t) => eqAddress(t.routerAddress, router));
}
export function mergeRoutes(routes: RoutesMap, newRoutes: Route[]) {
const mergedRoutes = deepCopy(routes);
for (const route of newRoutes) {
mergedRoutes[route.originCaip2Id] ||= {};
mergedRoutes[route.originCaip2Id][route.destCaip2Id] ||= [];
mergedRoutes[route.originCaip2Id][route.destCaip2Id].push(route);
}
return mergedRoutes;
}

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

@ -1,6 +1,7 @@
{
"compilerOptions": {
"allowJs": true,
"allowImportingTsExtensions":true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
@ -27,7 +28,9 @@
},
"exclude": ["node_modules", "dist"],
"include": ["next-env.d.ts", "./src/"],
"files": ["./src/global.d.ts"],
"ts-node": {
"files": true,
"compilerOptions": {
"module": "commonjs"
}

Loading…
Cancel
Save