diff --git a/.vscode/settings.json b/.vscode/settings.json index 4983c800..91ea6ca4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,14 +16,14 @@ "[typescript]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode" }, @@ -38,9 +38,4 @@ }, "editor.tabSize": 2, "editor.detectIndentation": false, - "[typescript][typescriptreact]": { - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" - } - }, } diff --git a/package.json b/package.json index 5367a54e..c6940b9d 100644 --- a/package.json +++ b/package.json @@ -78,13 +78,12 @@ }, "scripts": { "clean": "rm -rf dist cache .next", - "dev": "yarn build:configs && next dev", - "build": "yarn build:configs && next build", - "build:configs": "./src/scripts/buildConfigs/build.sh", + "dev": "next dev", + "build": "next build", "typecheck": "tsc", "lint": "next lint", "start": "next start", - "test": "yarn build:configs && jest", + "test": "jest", "prettier": "prettier --write ./src" }, "types": "dist/src/index.d.ts", diff --git a/src/components/buttons/ConnectAwareSubmitButton.tsx b/src/components/buttons/ConnectAwareSubmitButton.tsx index 2422887f..cc569146 100644 --- a/src/components/buttons/ConnectAwareSubmitButton.tsx +++ b/src/components/buttons/ConnectAwareSubmitButton.tsx @@ -3,24 +3,24 @@ import { useCallback } from 'react'; import { ProtocolType } from '@hyperlane-xyz/utils'; -import { tryGetProtocolType } from '../../features/caip/chains'; +import { tryGetChainProtocol } from '../../features/chains/utils'; import { useAccountForChain, useConnectFns } from '../../features/wallet/hooks/multiProtocol'; import { useTimeout } from '../../utils/timeout'; import { SolidButton } from './SolidButton'; interface Props { - chainCaip2Id: ChainCaip2Id; + chainName: ChainName; text: string; classes?: string; } -export function ConnectAwareSubmitButton({ chainCaip2Id, text, classes }: Props) { - const protocol = tryGetProtocolType(chainCaip2Id) || ProtocolType.Ethereum; +export function ConnectAwareSubmitButton({ chainName, text, classes }: Props) { + const protocol = tryGetChainProtocol(chainName) || ProtocolType.Ethereum; const connectFns = useConnectFns(); const connectFn = connectFns[protocol]; - const account = useAccountForChain(chainCaip2Id); + const account = useAccountForChain(chainName); const isAccountReady = account?.isReady; const { errors, setErrors, touched, setTouched } = useFormikContext(); diff --git a/src/components/icons/ChainLogo.tsx b/src/components/icons/ChainLogo.tsx index 8aec8ce3..7bfe56c0 100644 --- a/src/components/icons/ChainLogo.tsx +++ b/src/components/icons/ChainLogo.tsx @@ -1,42 +1,27 @@ import Image from 'next/image'; import { ComponentProps, useMemo } from 'react'; -import { isNumeric } from '@hyperlane-xyz/utils'; import { ChainLogo as ChainLogoInner } from '@hyperlane-xyz/widgets'; -import { parseCaip2Id } from '../../features/caip/chains'; -import { getChainDisplayName } from '../../features/chains/utils'; -import { getMultiProvider } from '../../features/multiProvider'; -import { logger } from '../../utils/logger'; +import { getChainDisplayName, tryGetChainMetadata } from '../../features/chains/utils'; -type Props = Omit, 'chainId' | 'chainName'> & { - chainCaip2Id?: ChainCaip2Id; -}; +export function ChainLogo(props: ComponentProps) { + const { chainName, ...rest } = props; + const { chainId, chainDisplayName, icon } = useMemo(() => { + if (!chainName) return {}; + const chainDisplayName = getChainDisplayName(chainName); + const logoUri = tryGetChainMetadata(chainName)?.logoURI; + const icon = logoUri + ? (props: { width: number; height: number; title?: string }) => ( + + ) + : undefined; + return { + chainId, + chainDisplayName, + icon, + }; + }, [chainName]); -export function ChainLogo(props: Props) { - const { chainCaip2Id, ...rest } = props; - const { chainId, chainName, icon } = useMemo(() => { - if (!chainCaip2Id) return {}; - try { - const { reference } = parseCaip2Id(chainCaip2Id); - const chainId = isNumeric(reference) ? parseInt(reference, 10) : undefined; - const chainName = getChainDisplayName(chainCaip2Id); - const logoUri = getMultiProvider().tryGetChainMetadata(reference)?.logoURI; - const icon = logoUri - ? (props: { width: number; height: number; title?: string }) => ( - - ) - : undefined; - return { - chainId, - chainName, - icon, - }; - } catch (error) { - logger.error('Failed to parse caip2 id', error); - return {}; - } - }, [chainCaip2Id]); - - return ; + return ; } diff --git a/src/components/icons/TokenIcon.tsx b/src/components/icons/TokenIcon.tsx index 414e4370..a3666711 100644 --- a/src/components/icons/TokenIcon.tsx +++ b/src/components/icons/TokenIcon.tsx @@ -1,15 +1,14 @@ import Image from 'next/image'; import { memo } from 'react'; +import { Token } from '@hyperlane-xyz/sdk'; import { Circle } from '@hyperlane-xyz/widgets'; -import { getTokenAddress } from '../../features/caip/tokens'; -import { TokenMetadata } from '../../features/tokens/types'; import { isValidUrl } from '../../utils/url'; import { ErrorBoundary } from '../errors/ErrorBoundary'; interface Props { - token?: TokenMetadata; + token?: Token; size?: number; } @@ -20,9 +19,7 @@ function _TokenIcon({ token, size = 32 }: Props) { const fontSize = Math.floor(size / 2); const bgColorSeed = - token && !imageSrc - ? (Buffer.from(getTokenAddress(token.tokenCaip19Id)).at(0) || 0) % 5 - : undefined; + token && !imageSrc ? (Buffer.from(token.addressOrDenom).at(0) || 0) % 5 : undefined; return ( diff --git a/src/components/toast/TxSuccessToast.tsx b/src/components/toast/TxSuccessToast.tsx index b77f12d6..d9fa9a63 100644 --- a/src/components/toast/TxSuccessToast.tsx +++ b/src/components/toast/TxSuccessToast.tsx @@ -1,11 +1,10 @@ import { useMemo } from 'react'; import { toast } from 'react-toastify'; -import { parseCaip2Id } from '../../features/caip/chains'; -import { getMultiProvider } from '../../features/multiProvider'; +import { getMultiProvider } from '../../context/context'; -export function toastTxSuccess(msg: string, txHash: string, chainCaip2Id: ChainCaip2Id) { - toast.success(, { +export function toastTxSuccess(msg: string, txHash: string, chain: ChainName) { + toast.success(, { autoClose: 12000, }); } @@ -13,16 +12,15 @@ export function toastTxSuccess(msg: string, txHash: string, chainCaip2Id: ChainC export function TxSuccessToast({ msg, txHash, - chainCaip2Id, + chain, }: { msg: string; txHash: string; - chainCaip2Id: ChainCaip2Id; + chain: ChainName; }) { const url = useMemo(() => { - const { reference } = parseCaip2Id(chainCaip2Id); - return getMultiProvider().tryGetExplorerTxUrl(reference, { hash: txHash }); - }, [chainCaip2Id, txHash]); + return getMultiProvider().tryGetExplorerTxUrl(chain, { hash: txHash }); + }, [chain, txHash]); return (
diff --git a/src/consts/tokens.json b/src/consts/tokens.json index fe51488c..0967ef42 100644 --- a/src/consts/tokens.json +++ b/src/consts/tokens.json @@ -1 +1 @@ -[] +{} diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index 7b947e81..6c65f93d 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -1,30 +1,9 @@ -import { WarpTokenConfig } from '../features/tokens/types'; +import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; // A list of Warp UI token configs // Tokens can be defined here, in tokens.json, or in tokens.yaml // The input here is typically the output of the Hyperlane CLI warp deploy command -export const tokenList: WarpTokenConfig = [ - // Example collateral token for an EVM chain - { - type: 'collateral', - chainId: 5, - address: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - hypCollateralAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - name: 'Weth', - symbol: 'WETH', - decimals: 18, - logoURI: '/logos/weth.png', // See public/logos/ - }, - - // Example NFT (ERC721) token for an EVM chain - { - chainId: 5, - name: 'Test721', - symbol: 'TEST721', - decimals: 0, - type: 'collateral', - address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', - hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - isNft: true, - }, -]; +export const tokenConfigs: WarpCoreConfig = { + tokens: [], + options: {}, +}; diff --git a/src/consts/tokens.yaml b/src/consts/tokens.yaml index e0229f6d..4acaa42b 100644 --- a/src/consts/tokens.yaml +++ b/src/consts/tokens.yaml @@ -1,14 +1,24 @@ -# A list of Warp UI token configs -# Tokens can be defined here, in tokens.json, or in tokens.ts +# A list of Warp UI token configs and other options for the WarpCore +# Configs can be defined here, in tokens.json, or in tokens.ts # The input here is typically the output of the Hyperlane CLI warp deploy command --- -# Replace this [] with your token list -[] -# Example using a native token: -# - type: native -# chainId: 11155111 -# name: 'Ether' -# symbol: 'ETH' -# decimals: 18 -# hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949' -# logoURI: '/logos/weth.png' +tokens: + # Eth Mainnet HypNative token + - chainName: sepolia + standard: EvmHypNative + decimals: 18 + symbol: ETH + name: Ether + addressOrDenom: '0x767C51a91CC9dEF2F24C35c340649411D6390320' + logoURI: '/logos/weth.png' + connectedTokens: + - ethereum|alfajores|0x8bF6Ca2Dca1DF703Cb9144cef6A4d86abA7776C4 + - chainName: alfajores + standard: EvmHypSynthetic + decimals: 18 + symbol: ETH + name: Ether + addressOrDenom: '0x8bF6Ca2Dca1DF703Cb9144cef6A4d86abA7776C4' + logoURI: '/logos/weth.png' + connectedTokens: + - ethereum|sepolia|0x767C51a91CC9dEF2F24C35c340649411D6390320 diff --git a/src/context/README.md b/src/context/README.md deleted file mode 100644 index 5c3210b2..00000000 --- a/src/context/README.md +++ /dev/null @@ -1,4 +0,0 @@ -### 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. diff --git a/src/context/chains.ts b/src/context/chains.ts new file mode 100644 index 00000000..bced7e29 --- /dev/null +++ b/src/context/chains.ts @@ -0,0 +1,29 @@ +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 ChainsYaml from '../consts/chains.yaml'; +import { cosmosDefaultChain } from '../features/chains/cosmosDefault'; +import { logger } from '../utils/logger'; + +export const ChainConfigSchema = z.record( + ChainMetadataSchema.and(z.object({ mailbox: z.string().optional() })), +); + +export function getChainConfigs() { + // 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; + return { ...chainMetadata, ...customChainConfigs }; +} diff --git a/src/context/context.ts b/src/context/context.ts index 1ce378f2..e1bd770b 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,29 +1,19 @@ -import { ChainMap, ChainMetadata, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainMetadata, MultiProtocolProvider, WarpCore } 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'; +import { getChainConfigs } from './chains'; +import { getWarpCoreConfig } from './tokens'; export interface WarpContext { chains: ChainMap; - tokens: TokenMetadata[]; - routes: RoutesMap; multiProvider: MultiProtocolProvider<{ mailbox?: Address }>; + warpCore: WarpCore; } 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), - }; + warpContext = initWarpContext(); } return warpContext; } @@ -31,3 +21,23 @@ export function getWarpContext() { export function setWarpContext(context: WarpContext) { warpContext = context; } + +export function initWarpContext() { + const chains = getChainConfigs(); + const multiProvider = new MultiProtocolProvider<{ mailbox?: Address }>(chains); + const coreConfig = getWarpCoreConfig(); + const warpCore = WarpCore.FromConfig(multiProvider, coreConfig); + return { chains, multiProvider, warpCore }; +} + +export function getMultiProvider() { + return getWarpContext().multiProvider; +} + +export function getWarpCore() { + return getWarpContext().warpCore; +} + +export function getTokens() { + return getWarpCore().tokens; +} diff --git a/src/context/tokens.ts b/src/context/tokens.ts new file mode 100644 index 00000000..eb034a77 --- /dev/null +++ b/src/context/tokens.ts @@ -0,0 +1,19 @@ +import { WarpCoreConfig, WarpCoreConfigSchema } from '@hyperlane-xyz/sdk'; + +import TokensJson from '../consts/tokens.json'; +import { tokenConfigs as TokensTS } from '../consts/tokens.ts'; +import TokensYaml from '../consts/tokens.yaml'; +import { validateZodResult } from '../utils/zod.ts'; + +export function getWarpCoreConfig(): WarpCoreConfig { + const resultJson = WarpCoreConfigSchema.safeParse({ TokensJson }); + const configJson = validateZodResult(resultJson, 'warp core json config'); + const resultYaml = WarpCoreConfigSchema.safeParse({ TokensYaml }); + const configYaml = validateZodResult(resultYaml, 'warp core yaml config'); + const resultTs = WarpCoreConfigSchema.safeParse({ TokensTS }); + const configTs = validateZodResult(resultTs, 'warp core typescript config'); + + const tokens = [...configJson.tokens, ...configYaml.tokens, ...configTs.tokens]; + const options = { ...configJson.options, ...configYaml.options, ...configTs.options }; + return { tokens, options }; +} diff --git a/src/features/caip/chains.ts b/src/features/caip/chains.ts deleted file mode 100644 index 04e0627e..00000000 --- a/src/features/caip/chains.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ProtocolType } from '@hyperlane-xyz/utils'; - -import { logger } from '../../utils/logger'; - -// Based mostly on https://chainagnostic.org/CAIPs/caip-2 -// But uses different naming for the protocol -export function getCaip2Id(protocol: ProtocolType, reference: string | number): ChainCaip2Id { - if (!Object.values(ProtocolType).includes(protocol)) { - throw new Error(`Invalid chain environment: ${protocol}`); - } - if ( - ([ProtocolType.Ethereum, ProtocolType.Sealevel].includes(protocol) && - (typeof reference !== 'number' || reference <= 0)) || - (protocol === ProtocolType.Cosmos && typeof reference !== 'string') - ) { - throw new Error(`Invalid chain reference: ${reference}`); - } - return `${protocol}:${reference}`; -} - -export function parseCaip2Id(id: ChainCaip2Id) { - const [_protocol, reference] = id.split(':'); - const protocol = _protocol as ProtocolType; - if (!Object.values(ProtocolType).includes(protocol)) { - throw new Error(`Invalid chain protocol type: ${id}`); - } - if (!reference) { - throw new Error(`No reference found in caip2 id: ${id}`); - } - return { protocol, reference }; -} - -export function tryParseCaip2Id(id?: ChainCaip2Id) { - if (!id) return undefined; - try { - return parseCaip2Id(id); - } catch (err) { - logger.error(`Error parsing caip2 id ${id}`, err); - return undefined; - } -} - -export function getProtocolType(id: ChainCaip2Id) { - const { protocol } = parseCaip2Id(id); - return protocol; -} - -export function tryGetProtocolType(id?: ChainCaip2Id) { - return tryParseCaip2Id(id)?.protocol; -} - -export function getChainReference(id: ChainCaip2Id) { - const { reference } = parseCaip2Id(id); - return reference; -} - -export function tryGetChainReference(id?: ChainCaip2Id) { - return tryParseCaip2Id(id)?.reference; -} - -export function getEthereumChainId(id: ChainCaip2Id): number { - const { protocol, reference } = parseCaip2Id(id); - if (protocol !== ProtocolType.Ethereum) { - throw new Error(`Protocol type must be ethereum: ${id}`); - } - return parseInt(reference, 10); -} diff --git a/src/features/caip/tokens.ts b/src/features/caip/tokens.ts deleted file mode 100644 index 7b6ce4b2..00000000 --- a/src/features/caip/tokens.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ProtocolType, isValidAddress, isZeroishAddress } from '@hyperlane-xyz/utils'; - -import { COSMOS_ZERO_ADDRESS, EVM_ZERO_ADDRESS, SOL_ZERO_ADDRESS } from '../../consts/values'; -import { logger } from '../../utils/logger'; - -export enum AssetNamespace { - native = 'native', - erc20 = 'erc20', - erc721 = 'erc721', - spl = 'spl', // Solana Program Library standard token - spl2022 = 'spl2022', // Updated SPL version - ibcDenom = 'ibcDenom', -} - -// Based mostly on https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-19.md -// But uses simpler asset namespace naming for native tokens -export function getCaip19Id( - chainCaip2Id: ChainCaip2Id, - namespace: AssetNamespace, - address: Address, - tokenId?: string | number, -): TokenCaip19Id { - if (!Object.values(AssetNamespace).includes(namespace)) { - throw new Error(`Invalid asset namespace: ${namespace}`); - } - if (!isValidAddress(address) && !isZeroishAddress(address)) { - throw new Error(`Invalid address: ${address}`); - } - // NOTE: deviation from CAIP-19 spec here by separating token id with : instead of / - // Doing this because cosmos addresses use / all over the place - // The CAIP standard doesn't specify how to handle ibc / token factory addresses - return `${chainCaip2Id}/${namespace}:${address}${tokenId ? `:${tokenId}` : ''}`; -} - -export function parseCaip19Id(id: TokenCaip19Id) { - const segments = id.split('/'); - if (segments.length < 2) - throw new Error(`Invalid caip19 id: ${id}. Must have at least 2 main segments`); - - const chainCaip2Id = segments[0] as ChainCaip2Id; - const rest = segments.slice(1).join('/'); - const tokenSegments = rest.split(':'); - let namespace: AssetNamespace; - let address: Address; - let tokenId: string | undefined; - if (tokenSegments.length == 2) { - [namespace, address] = tokenSegments as [AssetNamespace, Address]; - } else if (tokenSegments.length == 3) { - // NOTE: deviation from CAIP-19 spec here by separating token id with : instead of / - // Doing this because cosmos addresses use / all over the place - // The CAIP standard doesn't specify how to handle ibc / token factory addresses - [namespace, address, tokenId] = tokenSegments as [AssetNamespace, Address, string]; - } else { - throw new Error(`Invalid caip19 id: ${id}. Must have 2 or 3 token segment`); - } - - if (!chainCaip2Id || !namespace || !address) - throw new Error(`Invalid caip19 id: ${id}. Segment values missing`); - - return { chainCaip2Id, namespace, address, tokenId }; -} - -export function tryParseCaip19Id(id?: TokenCaip19Id) { - if (!id) return undefined; - try { - return parseCaip19Id(id); - } catch (err) { - logger.error(`Error parsing caip2 id ${id}`, err); - return undefined; - } -} - -export function getChainIdFromToken(id: TokenCaip19Id): ChainCaip2Id { - return parseCaip19Id(id).chainCaip2Id; -} - -export function tryGetChainIdFromToken(id?: TokenCaip19Id): ChainCaip2Id | undefined { - return tryParseCaip19Id(id)?.chainCaip2Id; -} - -export function getAssetNamespace(id: TokenCaip19Id): AssetNamespace { - return parseCaip19Id(id).namespace as AssetNamespace; -} - -export function getTokenAddress(id: TokenCaip19Id): Address { - return parseCaip19Id(id).address; -} - -export function isNativeToken(id: TokenCaip19Id): boolean { - const { namespace } = parseCaip19Id(id); - return namespace === AssetNamespace.native; -} - -export function getNativeTokenAddress(protocol: ProtocolType): Address { - if (protocol === ProtocolType.Ethereum) { - return EVM_ZERO_ADDRESS; - } else if (protocol === ProtocolType.Sealevel) { - return SOL_ZERO_ADDRESS; - } else if (protocol === ProtocolType.Cosmos) { - return COSMOS_ZERO_ADDRESS; - } else { - throw new Error(`Unsupported protocol: ${protocol}`); - } -} - -export function isNonFungibleToken(id: TokenCaip19Id): boolean { - const { namespace } = parseCaip19Id(id); - return namespace === AssetNamespace.erc721; -} - -export function resolveAssetNamespace( - protocol: ProtocolType, - isNative?: boolean, - isNft?: boolean, - isSpl2022?: boolean, -) { - if (isNative) return AssetNamespace.native; - switch (protocol) { - case ProtocolType.Ethereum: - return isNft ? AssetNamespace.erc721 : AssetNamespace.erc20; - case ProtocolType.Sealevel: - return isSpl2022 ? AssetNamespace.spl2022 : AssetNamespace.spl; - case ProtocolType.Cosmos: - return AssetNamespace.ibcDenom; - default: - throw new Error(`Unsupported protocol: ${protocol}`); - } -} diff --git a/src/features/chains/ChainSelectField.tsx b/src/features/chains/ChainSelectField.tsx index 4535388f..e7a4a146 100644 --- a/src/features/chains/ChainSelectField.tsx +++ b/src/features/chains/ChainSelectField.tsx @@ -12,21 +12,21 @@ import { getChainDisplayName } from './utils'; type Props = { name: string; label: string; - chainCaip2Ids: ChainCaip2Id[]; - onChange?: (id: ChainCaip2Id) => void; + chains: ChainName[]; + onChange?: (id: ChainName) => void; disabled?: boolean; }; -export function ChainSelectField({ name, label, chainCaip2Ids, onChange, disabled }: Props) { - const [field, , helpers] = useField(name); +export function ChainSelectField({ name, label, chains, onChange, disabled }: Props) { + const [field, , helpers] = useField(name); const { setFieldValue } = useFormikContext(); - const handleChange = (newChainId: ChainCaip2Id) => { + const handleChange = (newChainId: ChainName) => { helpers.setValue(newChainId); // Reset other fields on chain change setFieldValue('recipientAddress', ''); setFieldValue('amount', ''); - setFieldValue('tokenCaip19Id', ''); + setFieldValue('token', ''); if (onChange) onChange(newChainId); }; @@ -40,7 +40,7 @@ export function ChainSelectField({ name, label, chainCaip2Ids, onChange, disable
- +
diff --git a/src/features/chains/ChainSelectModal.tsx b/src/features/chains/ChainSelectModal.tsx index 6fb02a6b..e0a8f179 100644 --- a/src/features/chains/ChainSelectModal.tsx +++ b/src/features/chains/ChainSelectModal.tsx @@ -6,17 +6,17 @@ import { getChainDisplayName } from './utils'; export function ChainSelectListModal({ isOpen, close, - chainCaip2Ids, + chains, onSelect, }: { isOpen: boolean; close: () => void; - chainCaip2Ids: ChainCaip2Id[]; - onSelect: (chainCaip2Id: ChainCaip2Id) => void; + chains: ChainName[]; + onSelect: (chain: ChainName) => void; }) { - const onSelectChain = (chainCaip2Id: ChainCaip2Id) => { + const onSelectChain = (chain: ChainName) => { return () => { - onSelect(chainCaip2Id); + onSelect(chain); close(); }; }; @@ -24,13 +24,13 @@ export function ChainSelectListModal({ return (
- {chainCaip2Ids.map((c) => ( + {chains.map((c) => ( ))} diff --git a/src/features/chains/utils.ts b/src/features/chains/utils.ts index 85d54404..eb61fdfe 100644 --- a/src/features/chains/utils.ts +++ b/src/features/chains/utils.ts @@ -1,25 +1,22 @@ -import { chainIdToMetadata } from '@hyperlane-xyz/sdk'; +import { chainMetadata } from '@hyperlane-xyz/sdk'; import { ProtocolType, toTitleCase } from '@hyperlane-xyz/utils'; -import { parseCaip2Id } from '../caip/chains'; -import { getMultiProvider } from '../multiProvider'; +import { getMultiProvider } from '../../context/context'; -export function getChainDisplayName(id: ChainCaip2Id, shortName = false) { - if (!id) return 'Unknown'; - const { reference } = parseCaip2Id(id); - const metadata = getMultiProvider().tryGetChainMetadata(reference || 0); +export function getChainDisplayName(chain: ChainName, shortName = false) { + if (!chain) return 'Unknown'; + const metadata = tryGetChainMetadata(chain); if (!metadata) return 'Unknown'; const displayName = shortName ? metadata.displayNameShort : metadata.displayName; return displayName || metadata.displayName || toTitleCase(metadata.name); } -export function isPermissionlessChain(id: ChainCaip2Id) { - if (!id) return true; - const { protocol, reference } = parseCaip2Id(id); - return protocol !== ProtocolType.Ethereum || !chainIdToMetadata[reference]; +export function isPermissionlessChain(chain: ChainName) { + if (!chain) return true; + return getChainMetadata(chain).protocol === ProtocolType.Ethereum || !chainMetadata[chain]; } -export function hasPermissionlessChain(ids: ChainCaip2Id[]) { +export function hasPermissionlessChain(ids: ChainName[]) { return !ids.every((c) => !isPermissionlessChain(c)); } @@ -30,3 +27,19 @@ export function getChainByRpcEndpoint(endpoint?: string) { (m) => !!m.rpcUrls.find((rpc) => rpc.http.toLowerCase().includes(endpoint.toLowerCase())), ); } + +export function tryGetChainMetadata(chain: ChainName) { + return getMultiProvider().tryGetChainMetadata(chain); +} + +export function getChainMetadata(chain: ChainName) { + return getMultiProvider().getChainMetadata(chain); +} + +export function tryGetChainProtocol(chain: ChainName) { + return tryGetChainMetadata(chain)?.protocol; +} + +export function getChainProtocol(chain: ChainName) { + return getChainMetadata(chain).protocol; +} diff --git a/src/features/core/Token.ts b/src/features/core/Token.ts deleted file mode 100644 index 811dc488..00000000 --- a/src/features/core/Token.ts +++ /dev/null @@ -1,267 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ -import { - ChainMetadata, - ChainName, - CosmIbcTokenAdapter, - CosmNativeTokenAdapter, - CwHypCollateralAdapter, - CwHypNativeAdapter, - CwHypSyntheticAdapter, - CwNativeTokenAdapter, - CwTokenAdapter, - EvmHypCollateralAdapter, - EvmHypSyntheticAdapter, - EvmNativeTokenAdapter, - EvmTokenAdapter, - IHypTokenAdapter, - ITokenAdapter, - MultiProtocolProvider, - SealevelHypCollateralAdapter, - SealevelHypNativeAdapter, - SealevelHypSyntheticAdapter, - SealevelNativeTokenAdapter, - SealevelTokenAdapter, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType, eqAddress } from '@hyperlane-xyz/utils'; - -import { TokenAmount } from './TokenAmount'; -import { - Numberish, - PROTOCOL_TO_NATIVE_STANDARD, - TOKEN_HYP_STANDARDS, - TOKEN_MULTI_CHAIN_STANDARDS, - TOKEN_NFT_STANDARDS, - TokenStandard, -} from './types'; - -export interface TokenArgs { - protocol: ProtocolType; - chainName: ChainName; - standard: TokenStandard; - addressOrDenom: Address | string; - collateralAddressOrDenom?: Address | string; - igpTokenAddressOrDenom?: string; - decimals: number; - symbol: string; - name: string; - logoURI?: string; - connectedTokens?: Token[]; - - // Cosmos specific: - sourcePort?: string; - sourceChannel?: string; -} - -// Declaring the interface in addition to class allows -// Typescript to infer the members vars from TokenArgs -export interface Token extends TokenArgs {} - -export class Token { - constructor(public readonly args: TokenArgs) { - Object.assign(this, args); - } - - static FromChainMetadataNativeToken(chainMetadata: ChainMetadata): Token { - if (!chainMetadata.nativeToken) - throw new Error(`ChainMetadata for ${chainMetadata.name} missing nativeToken`); - - const { protocol, name: chainName, nativeToken, logoURI } = chainMetadata; - return new Token({ - protocol, - chainName, - standard: PROTOCOL_TO_NATIVE_STANDARD[protocol], - addressOrDenom: '', - decimals: nativeToken.decimals, - symbol: nativeToken.symbol, - name: nativeToken.name, - logoURI, - }); - } - - /** - * Returns a TokenAdapter for the token and multiProvider - * @throws If multiProvider does not contain this token's chain. - * @throws If token is an NFT (TODO NFT Adapter support) - */ - getAdapter(multiProvider: MultiProtocolProvider): ITokenAdapter { - const { standard, chainName, addressOrDenom } = this; - - if (this.isNft()) throw new Error('NFT adapters not yet supported'); - if (!multiProvider.tryGetChainMetadata(chainName)) - throw new Error(`Token chain ${chainName} not found in multiProvider`); - - if (standard === TokenStandard.ERC20) { - return new EvmTokenAdapter(chainName, multiProvider, { token: addressOrDenom }); - } else if (standard === TokenStandard.EvmNative) { - return new EvmNativeTokenAdapter(chainName, multiProvider, {}); - } else if (standard === TokenStandard.SealevelSpl) { - return new SealevelTokenAdapter(chainName, multiProvider, { token: addressOrDenom }, false); - } else if (standard === TokenStandard.SealevelSpl2022) { - return new SealevelTokenAdapter(chainName, multiProvider, { token: addressOrDenom }, true); - } else if (standard === TokenStandard.SealevelNative) { - return new SealevelNativeTokenAdapter(chainName, multiProvider, {}); - } else if (standard === TokenStandard.CosmosIcs20) { - throw new Error('Cosmos ICS20 token adapter not yet supported'); - } else if (standard === TokenStandard.CosmosNative) { - return new CosmNativeTokenAdapter(chainName, multiProvider, {}, { ibcDenom: addressOrDenom }); - } else if (standard === TokenStandard.CosmosFactory) { - throw new Error('Cosmos factory token adapter not yet supported'); - } else if (standard === TokenStandard.CW20) { - // TODO pass in denom here somehow - return new CwTokenAdapter(chainName, multiProvider, { token: addressOrDenom }); - } else if (standard === TokenStandard.CWNative) { - return new CwNativeTokenAdapter(chainName, multiProvider, {}, addressOrDenom); - } else if (this.isMultiChainToken()) { - return this.getHypAdapter(multiProvider); - } else { - throw new Error(`No adapter found for token standard: ${standard}`); - } - } - - /** - * Returns a HypTokenAdapter for the token and multiProvider - * @throws If not applicable to this token's standard. - * @throws If multiProvider does not contain this token's chain. - * @throws If token is an NFT (TODO NFT Adapter support) - */ - getHypAdapter(multiProvider: MultiProtocolProvider<{ mailbox?: Address }>): IHypTokenAdapter { - const { - protocol, - standard, - chainName, - addressOrDenom, - collateralAddressOrDenom, - igpTokenAddressOrDenom, - sourcePort, - sourceChannel, - } = this; - const chainMetadata = multiProvider.tryGetChainMetadata(chainName); - const mailbox = chainMetadata?.mailbox; - - if (!this.isMultiChainToken()) - throw new Error(`Token standard ${standard} not applicable to hyp adapter`); - if (this.isNft()) throw new Error('NFT adapters not yet supported'); - if (!chainMetadata) throw new Error(`Token chain ${chainName} not found in multiProvider`); - - let sealevelAddresses; - if (protocol === ProtocolType.Sealevel) { - if (!mailbox) throw new Error('mailbox required for Sealevel hyp tokens'); - if (!collateralAddressOrDenom) - throw new Error('collateralAddressOrDenom required for Sealevel hyp tokens'); - sealevelAddresses = { - warpRouter: addressOrDenom, - token: collateralAddressOrDenom, - mailbox, - }; - } - - if (standard === TokenStandard.EvmHypNative || standard === TokenStandard.EvmHypCollateral) { - return new EvmHypCollateralAdapter(chainName, multiProvider, { token: addressOrDenom }); - } else if (standard === TokenStandard.EvmHypSynthetic) { - return new EvmHypSyntheticAdapter(chainName, multiProvider, { token: addressOrDenom }); - } else if (standard === TokenStandard.SealevelHypNative) { - return new SealevelHypNativeAdapter(chainName, multiProvider, sealevelAddresses, false); - } else if (standard === TokenStandard.SealevelHypCollateral) { - return new SealevelHypCollateralAdapter(chainName, multiProvider, sealevelAddresses, false); - } else if (standard === TokenStandard.SealevelHypSynthetic) { - return new SealevelHypSyntheticAdapter(chainName, multiProvider, sealevelAddresses, false); - } else if (standard === TokenStandard.CwHypNative) { - return new CwHypNativeAdapter( - chainName, - multiProvider, - { warpRouter: addressOrDenom }, - igpTokenAddressOrDenom, - ); - } else if (standard === TokenStandard.CwHypCollateral) { - if (!collateralAddressOrDenom) - throw new Error('collateralAddressOrDenom required for CwHypCollateral'); - return new CwHypCollateralAdapter( - chainName, - multiProvider, - { warpRouter: addressOrDenom, token: collateralAddressOrDenom }, - igpTokenAddressOrDenom, - ); - } else if (standard === TokenStandard.CwHypSynthetic) { - if (!collateralAddressOrDenom) - throw new Error('collateralAddressOrDenom required for CwHypSyntheticAdapter'); - return new CwHypSyntheticAdapter( - chainName, - multiProvider, - { warpRouter: addressOrDenom, token: collateralAddressOrDenom }, - igpTokenAddressOrDenom, - ); - } else if (standard === TokenStandard.CosmosIbc) { - if (!sourcePort || !sourceChannel) - throw new Error('sourcePort and sourceChannel required for IBC token adapters'); - return new CosmIbcTokenAdapter( - chainName, - multiProvider, - {}, - { ibcDenom: addressOrDenom, sourcePort, sourceChannel }, - ); - } else { - throw new Error(`No hyp adapter found for token standard: ${standard}`); - } - } - - /** - * Convenience method to create an adapter and return an account balance - */ - async getBalance(multiProvider: MultiProtocolProvider, address: Address): Promise { - const adapter = this.getAdapter(multiProvider); - const balance = await adapter.getBalance(address); - return new TokenAmount(balance, this); - } - - amount(amount: Numberish): TokenAmount { - return new TokenAmount(amount, this); - } - - isNft(): boolean { - return TOKEN_NFT_STANDARDS.includes(this.standard); - } - - isNative(): boolean { - return Object.values(PROTOCOL_TO_NATIVE_STANDARD).includes(this.standard); - } - - isMultiChainToken(): boolean { - return TOKEN_MULTI_CHAIN_STANDARDS.includes(this.standard); - } - - getConnectedTokens(): Token[] { - return this.connectedTokens || []; - } - - setConnectedTokens(tokens: Token[]): Token[] { - this.connectedTokens = tokens; - return tokens; - } - - /** - * Returns true if tokens refer to the same asset - */ - equals(token: Token): boolean { - return ( - this.protocol === token.protocol && - this.chainName === token.chainName && - this.standard === token.standard && - this.decimals === token.decimals && - this.addressOrDenom.toLowerCase() === token.addressOrDenom.toLowerCase() && - this.collateralAddressOrDenom?.toLowerCase() === token.collateralAddressOrDenom?.toLowerCase() - ); - } - - /** - * Returns true if this tokens is hyp collateral contract for the given token - */ - collateralizes(token: Token): boolean { - if (token.chainName !== this.chainName) return false; - if (!TOKEN_HYP_STANDARDS.includes(this.standard)) return false; - const isCollateralWrapper = - this.collateralAddressOrDenom && - eqAddress(this.collateralAddressOrDenom, token.addressOrDenom); - const isNativeWrapper = !this.collateralAddressOrDenom && token.isNative(); - return isCollateralWrapper || isNativeWrapper; - } -} diff --git a/src/features/core/TokenAmount.ts b/src/features/core/TokenAmount.ts deleted file mode 100644 index d1470f43..00000000 --- a/src/features/core/TokenAmount.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { fromWei } from '@hyperlane-xyz/utils'; - -import type { Token } from './Token'; -import { Numberish } from './types'; - -export class TokenAmount { - public readonly amount: bigint; - - constructor(_amount: Numberish, public readonly token: Token) { - this.amount = BigInt(_amount); - } - - getDecimalFormattedAmount(): number { - return Number(fromWei(this.amount.toString(), this.token.decimals)); - } - - plus(amount: Numberish): TokenAmount { - return new TokenAmount(this.amount + BigInt(amount), this.token); - } - - minus(amount: Numberish): TokenAmount { - return new TokenAmount(this.amount - BigInt(amount), this.token); - } - - equals(tokenAmount: TokenAmount): boolean { - return this.amount === tokenAmount.amount && this.token.equals(tokenAmount.token); - } -} diff --git a/src/features/core/WarpCore.ts b/src/features/core/WarpCore.ts deleted file mode 100644 index b11ef8ee..00000000 --- a/src/features/core/WarpCore.ts +++ /dev/null @@ -1,289 +0,0 @@ -import debug, { Debugger } from 'debug'; - -import { ERC20__factory, ERC721__factory } from '@hyperlane-xyz/core'; -import { ChainName, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; -import { ProtocolType, eqAddress, isValidAddress } from '@hyperlane-xyz/utils'; - -import { Token } from './Token'; -import { TokenAmount } from './TokenAmount'; -import { HyperlaneChainId, IgpQuoteConstants, RouteBlacklist, TokenStandard } from './types'; - -export interface WarpCoreOptions { - loggerName?: string; - igpQuoteConstants?: IgpQuoteConstants; - routeBlacklist?: RouteBlacklist; -} - -export class WarpCore { - public readonly multiProvider: MultiProtocolProvider<{ mailbox?: Address }>; - public readonly tokens: Token[]; - public readonly igpQuoteConstants: IgpQuoteConstants; - public readonly routeBlacklist: RouteBlacklist; - public readonly logger: Debugger; - - constructor( - multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, - tokens: Token[], - options: WarpCoreOptions, - ) { - this.multiProvider = multiProvider; - this.tokens = tokens; - this.igpQuoteConstants = options?.igpQuoteConstants || []; - this.routeBlacklist = options?.routeBlacklist || []; - this.logger = debug(options?.loggerName || 'hyperlane:WarpCore'); - } - - // Takes the serialized representation of a complete warp config and returns a WarpCore instance - static FromConfig( - _multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, - _config: string, - ): WarpCore { - throw new Error('TODO: method not implemented'); - } - - async getTransferGasQuote( - originToken: Token, - destination: HyperlaneChainId, - ): Promise { - const { chainName: originName, protocol: originProtocol } = originToken; - const destinationName = this.multiProvider.getChainName(destination); - - // Step 1: Determine the amount - - let gasAmount: bigint; - // Check constant quotes first - const defaultQuote = this.igpQuoteConstants.find( - (q) => q.origin === originName && q.destination === destinationName, - ); - if (defaultQuote) { - gasAmount = BigInt(defaultQuote.toString()); - } else { - // Otherwise, compute IGP quote via the adapter - const hypAdapter = originToken.getHypAdapter(this.multiProvider); - const destinationDomainId = this.multiProvider.getDomainId(destination); - gasAmount = BigInt(await hypAdapter.quoteGasPayment(destinationDomainId)); - } - - // Step 2: Determine the IGP token - // TODO, it would be more robust to determine this based on on-chain data - // rather than these janky heuristic - - // If the token has an explicit IGP token address set, use that - let igpToken: Token; - if (originToken.igpTokenAddressOrDenom) { - const searchResult = this.findToken(originToken.igpTokenAddressOrDenom, originName); - if (!searchResult) - throw new Error(`IGP token ${originToken.igpTokenAddressOrDenom} is unknown`); - igpToken = searchResult; - } else if (originProtocol === ProtocolType.Cosmos) { - // If the protocol is cosmos, assume the token itself is used - igpToken = originToken; - } else { - // Otherwise use the plain old native token from the route origin - igpToken = Token.FromChainMetadataNativeToken( - this.multiProvider.getChainMetadata(originName), - ); - } - - this.logger(`Quoted igp gas payment: ${gasAmount} ${igpToken.symbol}`); - return new TokenAmount(gasAmount, igpToken); - } - - async getTransferRemoteTxs( - originTokenAmount: TokenAmount, - destination: HyperlaneChainId, - sender: Address, - recipient: Address, - ): Promise<{ approveTx; transferTx }> { - const { token, amount } = originTokenAmount; - const destinationDomainId = this.multiProvider.getDomainId(destination); - const hypAdapter = token.getHypAdapter(this.multiProvider); - - let approveTx: any = undefined; - if (await this.isApproveRequired(originTokenAmount, sender)) { - this.logger(`Approval required for transfer of ${token.symbol}`); - approveTx = await hypAdapter.populateApproveTx({ - weiAmountOrId: amount.toString(), - recipient: token.addressOrDenom, - }); - } - - const igpQuote = await this.getTransferGasQuote(token, destination); - - // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together - // TODO decide how to handle txValue here - // const txValue = - // (token.equals(igpQuote.token) || token.collateralizes(igpQuote.token)) - // ? BigNumber(igpQuote.weiAmount).plus(weiAmountOrId).toFixed(0) - // : igpQuote.weiAmount; - - const transferTx = await hypAdapter.populateTransferRemoteTx({ - weiAmountOrId: amount.toString(), - destination: destinationDomainId, - recipient, - // TODO - txValue: igpQuote.amount.toString(), - }); - - return { approveTx, transferTx }; - } - - /** - * Checks if destination chain's collateral is sufficient to cover the transfer - */ - async isDestinationCollateralSufficient( - originTokenAmount: TokenAmount, - destination: HyperlaneChainId, - ): Promise { - throw new Error('TODO'); - } - - /** - * Checks if a token transfer requires an approval tx first - */ - async isApproveRequired(originTokenAmount: TokenAmount, sender: Address) { - const { token, amount } = originTokenAmount; - const tokenAddress = token.addressOrDenom; - if (token.standard !== TokenStandard.EvmHypCollateral) { - return false; - } - - const provider = this.multiProvider.getEthersV5Provider(token.chainName); - let isRequired: boolean; - if (token.isNft()) { - const contract = ERC721__factory.connect(tokenAddress, provider); - const approvedAddress = await contract.getApproved(amount); - isRequired = !eqAddress(approvedAddress, tokenAddress); - } else { - const contract = ERC20__factory.connect(tokenAddress, provider); - const allowance = await contract.allowance(sender, tokenAddress); - isRequired = allowance.lt(amount); - } - this.logger(`Approval is${isRequired ? '' : ' not'} required for transfer of ${token.symbol}`); - return isRequired; - } - - /** - * Ensure the remote token transfer would be valid for the given chains, amount, sender, and recipient - */ - async validateTransfer( - originTokenAmount: TokenAmount, - destination: HyperlaneChainId, - sender: Address, - recipient: Address, - ): Promise | null> { - const chainError = this.validateChains(originTokenAmount.token.chainName, destination); - if (chainError) return chainError; - - const recipientError = this.validateRecipient(recipient, destination); - if (recipientError) return recipientError; - - const amountError = this.validateAmount(originTokenAmount); - if (amountError) return amountError; - - const balancesError = await this.validateTokenBalances(originTokenAmount, destination, sender); - if (balancesError) return balancesError; - - return null; - } - - /** - * Ensure the origin and destination chains are valid and known by this WarpCore - */ - protected validateChains( - origin: HyperlaneChainId, - destination: HyperlaneChainId, - ): Record | null { - if (!origin) return { origin: 'Origin chain required' }; - if (!destination) return { destination: 'Destination chain required' }; - const originMetadata = this.multiProvider.tryGetChainMetadata(origin); - const destinationMetadata = this.multiProvider.tryGetChainMetadata(destination); - if (!originMetadata) return { origin: 'Origin chain metadata missing' }; - if (!destinationMetadata) return { destination: 'Destination chain metadata missing' }; - if ( - this.routeBlacklist.some( - (bl) => bl.origin === originMetadata?.name && bl.destination === destinationMetadata.name, - ) - ) { - return { destination: 'Route is not currently allowed' }; - } - return null; - } - - /** - * Ensure recipient address is valid for the destination chain - */ - protected validateRecipient( - recipient: Address, - destination: HyperlaneChainId, - ): Record | null { - const destinationMetadata = this.multiProvider.getChainMetadata(destination); - // Ensure recip address is valid for the destination chain's protocol - if (!isValidAddress(recipient, destinationMetadata.protocol)) - return { recipient: 'Invalid recipient' }; - // Also ensure the address denom is correct if the dest protocol is Cosmos - if (destinationMetadata.protocol === ProtocolType.Cosmos) { - if (!destinationMetadata.bech32Prefix) { - this.logger(`No bech32 prefix found for chain ${destination}`); - return { destination: 'Invalid chain data' }; - } else if (!recipient.startsWith(destinationMetadata.bech32Prefix)) { - this.logger(`Recipient address prefix should be ${destination}`); - return { recipient: `Invalid recipient prefix` }; - } - } - return null; - } - - /** - * Ensure token amount is valid - */ - protected validateAmount(originTokenAmount: TokenAmount): Record | null { - if (!originTokenAmount.amount || originTokenAmount.amount < 0n) { - const isNft = originTokenAmount.token.isNft(); - return { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' }; - } - return null; - } - - /** - * Ensure the sender has sufficient balances for transfer and interchain gas - */ - protected async validateTokenBalances( - originTokenAmount: TokenAmount, - destination: HyperlaneChainId, - sender: Address, - ): Promise | null> { - const { token, amount } = originTokenAmount; - const { amount: senderBalance } = await token.getBalance(this.multiProvider, sender); - - // First check basic token balance - if (amount > senderBalance) return { amount: 'Insufficient balance' }; - - // Next, ensure balances can cover IGP fees - const igpQuote = await this.getTransferGasQuote(token, destination); - if (token.equals(igpQuote.token) || token.collateralizes(igpQuote.token)) { - const total = amount + igpQuote.amount; - if (senderBalance < total) return { amount: 'Insufficient balance for gas and transfer' }; - } else { - const igpTokenBalance = await igpQuote.token.getBalance(this.multiProvider, sender); - if (igpTokenBalance.amount < igpQuote.amount) - return { amount: `Insufficient ${igpQuote.token.symbol} for gas` }; - } - - return null; - } - - /** - * Search through token list to find token with matching chain and address - */ - findToken(addressOrDenom: Address | string, chainName: ChainName): Token | null { - const results = this.tokens.filter( - (token) => - token.chainName === chainName && - token.addressOrDenom.toLowerCase() === addressOrDenom.toLowerCase(), - ); - if (!results.length) return null; - if (results.length > 1) throw new Error(`Ambiguous token search results for ${addressOrDenom}`); - return results[0]; - } -} diff --git a/src/features/core/WarpCoreSpec.ts b/src/features/core/WarpCoreSpec.ts deleted file mode 100644 index fc9657d8..00000000 --- a/src/features/core/WarpCoreSpec.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { ChainName } from "@hyperlane-xyz/sdk"; - -type HyperlaneChainId = ChainName | ChainId | DomainId; - -export enum TokenStandard { - // EVM - ERC20, - ERC721, - EvmNative, - EvmHypNative, - EvmHypCollateral, - EvmHypSynthetic, - - // Sealevel (Solana) - SealevelSpl, - SealevelSpl2022, - SealevelNative, - SealevelHypNative, - SealevelHypCollateral, - SealevelHypSynthetic, - - // Cosmos - CosmosIcs20, - CosmosIcs721, - CosmosNative, - CosmosIbc, - CosmosFactory, - - // CosmWasm - CW20, - CW721, - CwHypNative, - CwHypCollateral, - CwHypSynthetic, -} - -export class Token { - constructor({ - protocol, chainName, standard, addressOrDenom, collateralAddressOrDenom, - symbol, decimals, name, logoUri, connectedTokens - }) {} - - getAdapter(multiProvider): ITokenAdapter - getHypAdapter(multiProvider): IHypTokenAdapter // throws if not supported by standard - - getConnectedTokens(): Token[] - setConnectedTokens(tokens:Token[]): Token[] - addConnectedToken(token:Token): Token - removeConnectedToken(token:Token): Token - - amount(amount): TokenAmount - - isNft(): boolean - equals(token): boolean -} - -export class TokenAmount { - constructor({ amount, token }) - getAmount(): bigint - getDecimalFormattedAmount(): number - plus(amount): TokenAmount - minus(amount): TokenAmount - equals(amount): boolean -} - -export class WarpCore { - constructor({ - // Note, there's no ChainManager here because MultiProvider extends ChainMetadataManager and serves that function - multiProvider: MultiProtocolProvider<{ mailbox?: Address }>, - tokens: Token[], - }) - - // Takes the serialized representation of a complete warp config and returns a WarpCore instance - static FromConfig(config:string, multiProvider): WarpCore - - async getTransferGasQuote(originTokenAmount: TokenAmount, destination: HyperlaneChainId): - Promise<{originGas:TokenAmount, interchainGas:TokenAmount}> - - async validateTransfer(originTokenAmount: TokenAmount, destination: HyperlaneChainId, - recipient:Address): Promise | null> - - async getTransferRemoteTxs(originTokenAmount: TokenAmount, destination: HyperlaneChainId, - recipient:Address): Promise<{approveTx, transferTx}> - - // Checks to ensure the destination chain's collateral is sufficient to cover the transfer - async isDestinationCollateralSufficient( - originTokenAmount: TokenAmount, destination: HyperlaneChainId - ): Promise -} - -// Converts the user-provided token, chain, and route configs into a fully -// specified warp config. It auto-maps routes and fills in missing token data. -// This keeps the user-facing configs succinct. -// ALTERNATIVELY: Maybe we should use a single, verbose config layer since configs often -// comes from the CLI anyway. Downside: can't respond easily to router enrollments. -export class WarpConfigBuilder { - constructor(chainConfig, tokenConfig) - async build(): WarpConfig -} - -/* -NOTES BELOW, FEEL FREE TO IGNORE - -Improvements: -============== -Improve modularity & testability -Handle IGP business logic in single place -Reduce protocol-specific tx crafting special casing -Make IBC vs Hyp routes substitutable -Kill concept of 'base tokens' -Kill cluster of utils functions for routes & tokens -Kill CAIP IDs and related utils -Improve NFT vs non-NFT substitutability - -Non-goals: -========== -Improve wallet hooks + integrations -Improve Warp UI UX - -Ideas: -====== -Smarter Token class (and a child Token?) -TokenManager -Smart Chain class (and a child WarpChain?) - -> these help avoid CAIP ids -Smarter Route class --> replace route utils with methods -Replace routes dictionary with graph - -> routes are nodes, tokens are edges -ToCAIP method on smart classes - -> or some other serialization method -GasQuote method - -> avoid direct-to-adapter igq quoting, as engine instead - -> copy over logic form current hook - -> Params: fromChain, toChain, fromToken, toToken, amount - -> Returns IGP quote and maybe also native gas quote -Maybe a toRoutes() method for backwards compat -Migrate useTokenTransfer logic into engine -Leverage HypCore classes to extract message IDs -Delivery checking logic -Break into smaller classes, use dependency injection - -> TokenManager, IGP, Transferer, Delivery checker -Validation - -> takes input and returns errors based on warp context - -> would need balances + gas quote + igp quote -*/ - -// export class TokenManager() { -// constructor({ multiProvider, tokens }) -// getToken(chain: HyperlaneChainId, addressOrDenom): Token -// getTokensByAddress(addressOrDenom): Token[] -// } - -// export class WarpRoute { -// constructor({ originChainName, originToken, destinationChainName, destinationToken }) - -// getOriginAdapter(): ITokenAdapter -// getDestinationAdapter(): ITokenAdapter - -// getOriginHypAdapter(): IHypTokenAdapter -// getDestinationHypAdapter(): IHypTokenAdapter - -// getOriginProtocol(): ProtocolType -// getDestinationProtocol(): ProtocolType -// } - -// export class WarpRouteManager { -// constructor({ multiProvider, routes }) - -// getRoutesFrom(origin: HyperlaneChainId): WarpRoute[] -// getRoutesTo(destination: HyperlaneChainId): WarpRoute[] - -// getRoute(origin, destination, originToken, destinationToken): WarpRoute -// hasRoute(origin, destination, originToken, destinationToken): boolean -// } diff --git a/src/features/core/types.ts b/src/features/core/types.ts deleted file mode 100644 index b1189a9e..00000000 --- a/src/features/core/types.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ChainName } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; - -export type HyperlaneChainId = ChainName | ChainId | DomainId; -export type Numberish = number | string | bigint; - -export enum TokenStandard { - // EVM - ERC20 = 'ERC20', - ERC721 = 'ERC721', - EvmNative = 'EvmNative', - EvmHypNative = 'EvmHypNative', - EvmHypCollateral = 'EvmHypCollateral', - EvmHypSynthetic = 'EvmHypSynthetic', - - // Sealevel (Solana) - SealevelSpl = 'SealevelSpl', - SealevelSpl2022 = 'SealevelSpl2022', - SealevelNative = 'SealevelNative', - SealevelHypNative = 'SealevelHypNative', - SealevelHypCollateral = 'SealevelHypCollateral', - SealevelHypSynthetic = 'SealevelHypSynthetic', - - // Cosmos - CosmosIcs20 = 'CosmosIcs20', - CosmosIcs721 = 'CosmosIcs721', - CosmosNative = 'CosmosNative', - CosmosIbc = 'CosmosIbc', - CosmosFactory = 'CosmosFactory', - - // CosmWasm - CW20 = 'CW20', - CWNative = 'CWNative', - CW721 = 'CW721', - CwHypNative = 'CwHypNative', - CwHypCollateral = 'CwHypCollateral', - CwHypSynthetic = 'CwHypSynthetic', - - // Fuel (TODO) - FuelNative = 'FuelNative', -} - -export const TOKEN_NFT_STANDARDS = [ - TokenStandard.ERC721, - TokenStandard.CosmosIcs721, - TokenStandard.CW721, - // TODO solana here -]; - -export const TOKEN_HYP_STANDARDS = [ - TokenStandard.EvmHypNative, - TokenStandard.EvmHypCollateral, - TokenStandard.EvmHypSynthetic, - TokenStandard.SealevelHypNative, - TokenStandard.SealevelHypCollateral, - TokenStandard.SealevelHypSynthetic, - TokenStandard.CwHypNative, - TokenStandard.CwHypCollateral, - TokenStandard.CwHypSynthetic, -]; - -export const TOKEN_MULTI_CHAIN_STANDARDS = [...TOKEN_HYP_STANDARDS, TokenStandard.CosmosIbc]; - -export const PROTOCOL_TO_NATIVE_STANDARD: Record = { - [ProtocolType.Ethereum]: TokenStandard.EvmNative, - [ProtocolType.Cosmos]: TokenStandard.CosmosNative, - [ProtocolType.Sealevel]: TokenStandard.SealevelNative, - [ProtocolType.Fuel]: TokenStandard.FuelNative, -}; - -// Map of protocol to either quote constant or to a map of chain name to quote constant -export type IgpQuoteConstants = Array<{ - origin: ChainName; - destination: ChainName; - quote: string | number | bigint; -}>; - -export type RouteBlacklist = Array<{ origin: ChainName; destination: ChainName }>; diff --git a/src/features/multiProvider.ts b/src/features/multiProvider.ts deleted file mode 100644 index 6ee93371..00000000 --- a/src/features/multiProvider.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ProtocolType } from '@hyperlane-xyz/utils'; - -import { getWarpContext } from '../context/context'; - -import { parseCaip2Id } from './caip/chains'; - -export function getMultiProvider() { - return getWarpContext().multiProvider; -} - -export function getEvmProvider(id: ChainCaip2Id) { - const { reference, protocol } = parseCaip2Id(id); - if (protocol !== ProtocolType.Ethereum) throw new Error('Expected EVM chain for provider'); - // TODO viem - return getMultiProvider().getEthersV5Provider(reference); -} - -export function getSealevelProvider(id: ChainCaip2Id) { - const { reference, protocol } = parseCaip2Id(id); - if (protocol !== ProtocolType.Sealevel) throw new Error('Expected Sealevel chain for provider'); - return getMultiProvider().getSolanaWeb3Provider(reference); -} - -export function getCosmJsWasmProvider(id: ChainCaip2Id) { - const { reference, protocol } = parseCaip2Id(id); - if (protocol !== ProtocolType.Cosmos) throw new Error('Expected Cosmos chain for provider'); - return getMultiProvider().getCosmJsWasmProvider(reference); -} - -export function getChainMetadata(id: ChainCaip2Id) { - return getMultiProvider().getChainMetadata(parseCaip2Id(id).reference); -} diff --git a/src/features/routes/hooks.ts b/src/features/routes/hooks.ts deleted file mode 100644 index 64cb84a0..00000000 --- a/src/features/routes/hooks.ts +++ /dev/null @@ -1,19 +0,0 @@ -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]); -} diff --git a/src/features/routes/types.ts b/src/features/routes/types.ts deleted file mode 100644 index a5c05b5c..00000000 --- a/src/features/routes/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -export enum RouteType { - CollateralToCollateral = 'collateralToCollateral', - CollateralToSynthetic = 'collateralToSynthetic', - SyntheticToSynthetic = 'syntheticToSynthetic', - SyntheticToCollateral = 'syntheticToCollateral', - IbcNativeToIbcNative = 'ibcNativeToIbcNative', - IbcNativeToHypSynthetic = 'ibcNativeToHypSynthetic', -} - -interface BaseRoute { - type: RouteType; - // The underlying 'collateralized' token: - baseTokenCaip19Id: TokenCaip19Id; - originCaip2Id: ChainCaip2Id; - originDecimals: number; - destCaip2Id: ChainCaip2Id; - destDecimals: number; - // The underlying token on the destination chain - // Only set for CollateralToCollateral routes (b.c. sealevel needs it) - destTokenCaip19Id?: TokenCaip19Id; -} - -export interface WarpRoute extends BaseRoute { - type: - | RouteType.CollateralToCollateral - | RouteType.CollateralToSynthetic - | RouteType.SyntheticToCollateral - | RouteType.SyntheticToSynthetic; - baseRouterAddress: Address; - originRouterAddress: Address; - destRouterAddress: Address; -} - -interface BaseIbcRoute extends BaseRoute { - originIbcDenom: string; - sourcePort: string; - sourceChannel: string; - derivedIbcDenom: string; -} - -export interface IbcRoute extends BaseIbcRoute { - type: RouteType.IbcNativeToIbcNative; -} - -export interface IbcToWarpRoute extends BaseIbcRoute { - type: RouteType.IbcNativeToHypSynthetic; - intermediateCaip2Id: ChainCaip2Id; - intermediateRouterAddress: Address; - destRouterAddress: Address; -} - -export type Route = WarpRoute | IbcRoute | IbcToWarpRoute; - -export type RoutesMap = Record>; diff --git a/src/features/routes/utils.ts b/src/features/routes/utils.ts deleted file mode 100644 index 30846db9..00000000 --- a/src/features/routes/utils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isNativeToken } from '../caip/tokens'; - -import { IbcRoute, IbcToWarpRoute, Route, RouteType, RoutesMap, WarpRoute } from './types'; - -export function getTokenRoutes( - originCaip2Id: ChainCaip2Id, - destinationCaip2Id: ChainCaip2Id, - tokenRoutes: RoutesMap, -): Route[] { - return tokenRoutes[originCaip2Id]?.[destinationCaip2Id] || []; -} - -export function getTokenRoute( - originCaip2Id: ChainCaip2Id, - destinationCaip2Id: ChainCaip2Id, - tokenCaip19Id: TokenCaip19Id, - tokenRoutes: RoutesMap, -): Route | undefined { - if (!tokenCaip19Id) return undefined; - return getTokenRoutes(originCaip2Id, destinationCaip2Id, tokenRoutes).find( - (r) => r.baseTokenCaip19Id === tokenCaip19Id, - ); -} - -export function hasTokenRoute( - originCaip2Id: ChainCaip2Id, - destinationCaip2Id: ChainCaip2Id, - tokenCaip19Id: TokenCaip19Id, - tokenRoutes: RoutesMap, -): boolean { - 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 - ); -} - -export function isRouteFromNative(route: Route) { - return isRouteFromCollateral(route) && isNativeToken(route.baseTokenCaip19Id); -} - -export function isWarpRoute(route: Route): route is WarpRoute { - return !isIbcRoute(route); -} - -export function isIbcRoute(route: Route): route is IbcRoute | IbcToWarpRoute { - return ( - route.type === RouteType.IbcNativeToIbcNative || - route.type === RouteType.IbcNativeToHypSynthetic - ); -} - -// Differs from isIbcRoute above in that it it's only true for routes that -// Never interact with Hyperlane routers at all -export function isIbcOnlyRoute(route: Route): route is IbcRoute { - return route.type === RouteType.IbcNativeToIbcNative; -} - -export function isIbcToWarpRoute(route: Route): route is IbcToWarpRoute { - return route.type === RouteType.IbcNativeToHypSynthetic; -} diff --git a/src/features/tokens/AdapterFactory.ts b/src/features/tokens/AdapterFactory.ts deleted file mode 100644 index b514b1bb..00000000 --- a/src/features/tokens/AdapterFactory.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { - ChainName, - CosmNativeTokenAdapter, - CwHypCollateralAdapter, - CwHypNativeAdapter, - CwHypSyntheticAdapter, - CwNativeTokenAdapter, - CwTokenAdapter, - EvmHypCollateralAdapter, - EvmHypSyntheticAdapter, - EvmNativeTokenAdapter, - EvmTokenAdapter, - IHypTokenAdapter, - ITokenAdapter, - MultiProtocolProvider, - SealevelHypCollateralAdapter, - SealevelHypNativeAdapter, - SealevelHypSyntheticAdapter, - SealevelNativeTokenAdapter, - SealevelTokenAdapter, -} from '@hyperlane-xyz/sdk'; -import { Address, ProtocolType, convertToProtocolAddress } from '@hyperlane-xyz/utils'; - -import { parseCaip2Id } from '../caip/chains'; -import { AssetNamespace, getChainIdFromToken, isNativeToken, parseCaip19Id } from '../caip/tokens'; -import { getMultiProvider } from '../multiProvider'; -import { Route } from '../routes/types'; -import { - isIbcRoute, - isIbcToWarpRoute, - isRouteFromCollateral, - isRouteFromSynthetic, - isRouteToCollateral, - isRouteToSynthetic, - isWarpRoute, -} from '../routes/utils'; - -import { getToken } from './metadata'; - -export class AdapterFactory { - static NativeAdapterFromChain( - chainCaip2Id: ChainCaip2Id, - useCosmNative = false, - adapterProperties?: any, - ): ITokenAdapter { - const { protocol, reference: chainId } = parseCaip2Id(chainCaip2Id); - const multiProvider = getMultiProvider(); - const chainName = multiProvider.getChainMetadata(chainId).name; - if (protocol == ProtocolType.Ethereum) { - return new EvmNativeTokenAdapter(chainName, multiProvider, {}); - } else if (protocol === ProtocolType.Sealevel) { - return new SealevelNativeTokenAdapter(chainName, multiProvider, {}); - } else if (protocol === ProtocolType.Cosmos) { - return useCosmNative - ? new CosmNativeTokenAdapter(chainName, multiProvider, {}, adapterProperties) - : new CwNativeTokenAdapter(chainName, multiProvider, {}); - } else { - throw new Error(`Unsupported protocol: ${protocol}`); - } - } - - static NativeAdapterFromRoute(route: Route, source: 'origin' | 'destination'): ITokenAdapter { - let useCosmNative = false; - let adapterProperties: any = undefined; - if (isIbcRoute(route)) { - useCosmNative = true; - adapterProperties = { - ibcDenom: source === 'origin' ? route.originIbcDenom : route.derivedIbcDenom, - sourcePort: route.sourcePort, - sourceChannel: route.sourceChannel, - }; - } - return AdapterFactory.NativeAdapterFromChain( - source === 'origin' ? route.originCaip2Id : route.destCaip2Id, - useCosmNative, - adapterProperties, - ); - } - - static TokenAdapterFromAddress(tokenCaip19Id: TokenCaip19Id): ITokenAdapter { - const { address, chainCaip2Id } = parseCaip19Id(tokenCaip19Id); - const { protocol, reference: chainId } = parseCaip2Id(chainCaip2Id); - const multiProvider = getMultiProvider(); - const chainName = multiProvider.getChainMetadata(chainId).name; - const isNative = isNativeToken(tokenCaip19Id); - if (protocol == ProtocolType.Ethereum) { - return isNative - ? new EvmNativeTokenAdapter(chainName, multiProvider, {}) - : new EvmTokenAdapter(chainName, multiProvider, { token: address }); - } else if (protocol === ProtocolType.Sealevel) { - return isNative - ? new SealevelNativeTokenAdapter(chainName, multiProvider, {}) - : new SealevelTokenAdapter(chainName, multiProvider, { token: address }); - } else if (protocol === ProtocolType.Cosmos) { - return isNative - ? new CwNativeTokenAdapter(chainName, multiProvider, {}) - : new CwTokenAdapter(chainName, multiProvider, { token: address }); - } else { - throw new Error(`Unsupported protocol: ${protocol}`); - } - } - - static HypCollateralAdapterFromAddress( - baseTokenCaip19Id: TokenCaip19Id, - routerAddress: Address, - ): IHypTokenAdapter { - const isNative = isNativeToken(baseTokenCaip19Id); - return AdapterFactory.selectHypAdapter( - getChainIdFromToken(baseTokenCaip19Id), - routerAddress, - baseTokenCaip19Id, - EvmHypCollateralAdapter, - isNative ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, - isNative ? CwHypNativeAdapter : CwHypCollateralAdapter, - ); - } - - static HypSyntheticTokenAdapterFromAddress( - baseTokenCaip19Id: TokenCaip19Id, - chainCaip2Id: ChainCaip2Id, - routerAddress: Address, - ): IHypTokenAdapter { - return AdapterFactory.selectHypAdapter( - chainCaip2Id, - routerAddress, - baseTokenCaip19Id, - EvmHypSyntheticAdapter, - SealevelHypSyntheticAdapter, - CwHypSyntheticAdapter, - ); - } - - static HypTokenAdapterFromRouteOrigin(route: Route): IHypTokenAdapter { - if (!isWarpRoute(route)) throw new Error('Route is not a hyp route'); - const { type, originCaip2Id, originRouterAddress, baseTokenCaip19Id } = route; - const isNative = isNativeToken(baseTokenCaip19Id); - if (isRouteFromCollateral(route)) { - return AdapterFactory.selectHypAdapter( - originCaip2Id, - originRouterAddress, - baseTokenCaip19Id, - EvmHypCollateralAdapter, - isNative ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, - isNative ? CwHypNativeAdapter : CwHypCollateralAdapter, - ); - } else if (isRouteFromSynthetic(route)) { - return AdapterFactory.selectHypAdapter( - originCaip2Id, - originRouterAddress, - baseTokenCaip19Id, - EvmHypSyntheticAdapter, - SealevelHypSyntheticAdapter, - CwHypSyntheticAdapter, - ); - } else { - throw new Error(`Unsupported route type: ${type}`); - } - } - - static HypTokenAdapterFromRouteDest(route: Route): IHypTokenAdapter { - if (!isWarpRoute(route) && !isIbcToWarpRoute(route)) - throw new Error('Route is not a hyp route'); - const { type, destCaip2Id, destRouterAddress, destTokenCaip19Id, baseTokenCaip19Id } = route; - const tokenCaip19Id = destTokenCaip19Id || baseTokenCaip19Id; - const isNative = isNativeToken(baseTokenCaip19Id); - if (isRouteToCollateral(route) || isIbcToWarpRoute(route)) { - return AdapterFactory.selectHypAdapter( - destCaip2Id, - destRouterAddress, - tokenCaip19Id, - EvmHypCollateralAdapter, - isNative ? SealevelHypNativeAdapter : SealevelHypCollateralAdapter, - isNative ? CwHypNativeAdapter : CwHypCollateralAdapter, - ); - } else if (isRouteToSynthetic(route)) { - return AdapterFactory.selectHypAdapter( - destCaip2Id, - destRouterAddress, - tokenCaip19Id, - EvmHypSyntheticAdapter, - SealevelHypSyntheticAdapter, - CwHypSyntheticAdapter, - ); - } else { - throw new Error(`Unsupported route type: ${type}`); - } - } - - protected static selectHypAdapter( - chainCaip2Id: ChainCaip2Id, - routerAddress: Address, - baseTokenCaip19Id: TokenCaip19Id, - EvmAdapter: new ( - chainName: ChainName, - mp: MultiProtocolProvider, - addresses: { token: Address }, - ) => IHypTokenAdapter, - SealevelAdapter: new ( - chainName: ChainName, - mp: MultiProtocolProvider, - addresses: { token: Address; warpRouter: Address; mailbox: Address }, - isSpl2022?: boolean, - ) => IHypTokenAdapter, - CosmosAdapter: new ( - chainName: ChainName, - mp: MultiProtocolProvider, - addresses: any, - gasDenom?: string, - ) => IHypTokenAdapter, - ): IHypTokenAdapter { - const { protocol, reference: chainId } = parseCaip2Id(chainCaip2Id); - const { address: baseTokenAddress, namespace } = parseCaip19Id(baseTokenCaip19Id); - const tokenMetadata = getToken(baseTokenCaip19Id); - if (!tokenMetadata) throw new Error(`Token metadata not found for ${baseTokenCaip19Id}`); - const multiProvider = getMultiProvider(); - const { name: chainName, mailbox, bech32Prefix } = multiProvider.getChainMetadata(chainId); - - if (protocol == ProtocolType.Ethereum) { - return new EvmAdapter(chainName, multiProvider, { - token: convertToProtocolAddress(routerAddress, protocol), - }); - } else if (protocol === ProtocolType.Sealevel) { - if (!mailbox) throw new Error('Mailbox address required for sealevel hyp adapter'); - return new SealevelAdapter( - chainName, - multiProvider, - { - token: convertToProtocolAddress(baseTokenAddress, protocol), - warpRouter: convertToProtocolAddress(routerAddress, protocol), - mailbox, - }, - namespace === AssetNamespace.spl2022, - ); - } else if (protocol === ProtocolType.Cosmos) { - if (!bech32Prefix) throw new Error('Bech32 prefix required for cosmos hyp adapter'); - return new CosmosAdapter( - chainName, - multiProvider, - { - token: convertToProtocolAddress(baseTokenAddress, protocol, bech32Prefix), - warpRouter: convertToProtocolAddress(routerAddress, protocol, bech32Prefix), - }, - tokenMetadata.igpTokenAddressOrDenom || baseTokenAddress, - ); - } else { - throw new Error(`Unsupported protocol: ${protocol}`); - } - } -} diff --git a/src/features/tokens/SelectOrInputTokenIds.tsx b/src/features/tokens/SelectOrInputTokenIds.tsx index bcfcd165..f82ddd09 100644 --- a/src/features/tokens/SelectOrInputTokenIds.tsx +++ b/src/features/tokens/SelectOrInputTokenIds.tsx @@ -1,65 +1,37 @@ import { useFormikContext } from 'formik'; +import { Token } from '@hyperlane-xyz/sdk'; + 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'; -export function SelectOrInputTokenIds({ - disabled, - tokenRoutes, -}: { - disabled: boolean; - tokenRoutes: RoutesMap; -}) { +// import { useContractSupportsTokenByOwner, useIsSenderNftOwner } from './balances'; + +export function SelectOrInputTokenIds({ disabled }: { disabled: boolean }) { const { - values: { originCaip2Id, tokenCaip19Id, destinationCaip2Id }, + values: { token }, } = useFormikContext(); - - const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); - - let activeToken = '' as TokenCaip19Id; - if (route?.type === RouteType.CollateralToSynthetic) { - // If the origin is the base chain, use the collateralized token for balance checking - activeToken = tokenCaip19Id; - } else if (route && isWarpRoute(route)) { - // Otherwise, use the synthetic token for balance checking - activeToken = getCaip19Id( - route.originCaip2Id, - AssetNamespace.erc721, - route.originRouterAddress, - ); - } - - const accountAddress = useAccountAddressForChain(originCaip2Id); - const { isContractAllowToGetTokenIds } = useContractSupportsTokenByOwner( - activeToken, - accountAddress, - ); + // const accountAddress = useAccountAddressForChain(origin); + // const { isContractAllowToGetTokenIds } = useContractSupportsTokenByOwner( + // activeToken, + // accountAddress, + // ); + const isContractAllowToGetTokenIds = true; return isContractAllowToGetTokenIds ? ( - + ) : ( - + ); } -function InputTokenId({ - disabled, - tokenCaip19Id, -}: { - disabled: boolean; - tokenCaip19Id: TokenCaip19Id; -}) { - const { - values: { amount }, - } = useFormikContext(); - useIsSenderNftOwner(tokenCaip19Id, amount); +function InputTokenId({ disabled }: { disabled: boolean; token: Token }) { + // const { + // values: { amount }, + // } = useFormikContext(); + // useIsSenderNftOwner(token, amount); return (
diff --git a/src/features/tokens/SelectTokenIdField.tsx b/src/features/tokens/SelectTokenIdField.tsx index 4ba4402e..4b6099cc 100644 --- a/src/features/tokens/SelectTokenIdField.tsx +++ b/src/features/tokens/SelectTokenIdField.tsx @@ -2,19 +2,19 @@ import { useField } from 'formik'; import Image from 'next/image'; import { useState } from 'react'; +import { Token } from '@hyperlane-xyz/sdk'; + import { Spinner } from '../../components/animation/Spinner'; import { Modal } from '../../components/layout/Modal'; import ChevronIcon from '../../images/icons/chevron-down.svg'; -import { useOriginTokenIdBalance } from './balances'; - type Props = { name: string; - tokenCaip19Id: TokenCaip19Id; + token: Token; disabled?: boolean; }; -export function SelectTokenIdField({ name, tokenCaip19Id, disabled }: Props) { +export function SelectTokenIdField({ name, disabled }: Props) { const [, , helpers] = useField(name); const [tokenId, setTokenId] = useState(undefined); const handleChange = (newTokenId: string) => { @@ -22,7 +22,9 @@ export function SelectTokenIdField({ name, tokenCaip19Id, disabled }: Props) { setTokenId(newTokenId); }; - const { isLoading, tokenIds } = useOriginTokenIdBalance(tokenCaip19Id); + // const { isLoading, tokenIds } = useOriginTokenIdBalance(tokenCaip19Id); + const isLoading = false; + const tokenIds = []; const [isModalOpen, setIsModalOpen] = useState(false); diff --git a/src/features/tokens/TokenListModal.tsx b/src/features/tokens/TokenListModal.tsx index edb78abe..fda40d32 100644 --- a/src/features/tokens/TokenListModal.tsx +++ b/src/features/tokens/TokenListModal.tsx @@ -1,33 +1,27 @@ import Image from 'next/image'; import { useMemo, useState } from 'react'; +import { Token } from '@hyperlane-xyz/sdk'; + import { TokenIcon } from '../../components/icons/TokenIcon'; import { TextInput } from '../../components/input/TextField'; import { Modal } from '../../components/layout/Modal'; 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 { TokenMetadata } from './types'; export function TokenListModal({ isOpen, close, onSelect, - originCaip2Id, - destinationCaip2Id, - tokenRoutes, + origin, + destination, }: { isOpen: boolean; close: () => void; - onSelect: (token: TokenMetadata) => void; - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - tokenRoutes: RoutesMap; + onSelect: (token: Token) => void; + origin: ChainName; + destination: ChainName; }) { const [search, setSearch] = useState(''); @@ -36,7 +30,7 @@ export function TokenListModal({ setSearch(''); }; - const onSelectAndClose = (token: TokenMetadata) => { + const onSelectAndClose = (token: Token) => { onSelect(token); onClose(); }; @@ -57,9 +51,8 @@ export function TokenListModal({ autoComplete="off" /> @@ -68,29 +61,22 @@ export function TokenListModal({ } export function TokenList({ - originCaip2Id, - destinationCaip2Id, - tokenRoutes, + origin, + destination, searchQuery, onSelect, }: { - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - tokenRoutes: RoutesMap; + origin: ChainName; + destination: ChainName; searchQuery: string; - onSelect: (token: TokenMetadata) => void; + onSelect: (token: Token) => void; }) { const tokens = useMemo(() => { const q = searchQuery?.trim().toLowerCase(); return ( getTokens() .map((t) => { - const hasRoute = hasTokenRoute( - originCaip2Id, - destinationCaip2Id, - t.tokenCaip19Id, - tokenRoutes, - ); + const hasRoute = hasTokenRoute(origin, destination, t.tokenCaip19Id, tokenRoutes); return { ...t, disabled: !hasRoute }; }) .sort((a, b) => { @@ -112,7 +98,7 @@ export function TokenList({ // Hide/show disabled tokens .filter((t) => (config.showDisabledTokens ? true : !t.disabled)) ); - }, [searchQuery, originCaip2Id, destinationCaip2Id, tokenRoutes]); + }, [searchQuery, origin, destination, tokenRoutes]); return (
@@ -153,8 +139,8 @@ export function TokenList({ className="ml-auto mr-1" data-te-toggle="tooltip" title={`Route not supported for ${getChainDisplayName( - originCaip2Id, - )} to ${getChainDisplayName(destinationCaip2Id)}`} + origin, + )} to ${getChainDisplayName(destination)}`} /> )} diff --git a/src/features/tokens/TokenSelectField.tsx b/src/features/tokens/TokenSelectField.tsx index babea6e9..e27f2db1 100644 --- a/src/features/tokens/TokenSelectField.tsx +++ b/src/features/tokens/TokenSelectField.tsx @@ -2,68 +2,46 @@ import { useFormikContext } from 'formik'; import Image from 'next/image'; import { useEffect, useState } from 'react'; +import { Token } from '@hyperlane-xyz/sdk'; + 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 { TokenMetadata } from './types'; type Props = { name: string; - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - tokenRoutes: RoutesMap; + origin: ChainName; + destination: ChainName; disabled?: boolean; setIsNft: (value: boolean) => void; }; -export function TokenSelectField({ - name, - originCaip2Id, - destinationCaip2Id, - tokenRoutes, - disabled, - setIsNft, -}: Props) { +export function TokenSelectField({ name, origin, destination, disabled, setIsNft }: Props) { const { values, setFieldValue } = useFormikContext(); - // Keep local state for token details, but let formik manage field value - const [token, setToken] = useState(undefined); const [isModalOpen, setIsModalOpen] = useState(false); const [isAutomaticSelection, setIsAutomaticSelection] = useState(false); - // Keep local state in sync with formik state useEffect(() => { - const routes = getTokenRoutes(originCaip2Id, destinationCaip2Id, tokenRoutes); - let newFieldValue: TokenCaip19Id | undefined = undefined; - let newToken: TokenMetadata | undefined = undefined; + let newFieldValue: Token | undefined = undefined; let newIsAutomatic = true; if (routes.length === 1) { - newFieldValue = routes[0].baseTokenCaip19Id; - newToken = getToken(newFieldValue); + newFieldValue = 'TODO'; } else if (routes.length > 1) { - newFieldValue = values[name] || routes[0].baseTokenCaip19Id; - newToken = getToken(newFieldValue!); + newFieldValue = 'TODO'; newIsAutomatic = false; } - setToken(newToken); setFieldValue(name, newFieldValue || ''); setIsAutomaticSelection(newIsAutomatic); - }, [name, token, values, originCaip2Id, destinationCaip2Id, tokenRoutes, setFieldValue]); + }, [name, values, origin, destination, setFieldValue]); - const onSelectToken = (newToken: TokenMetadata) => { + const onSelectToken = (newToken: Token) => { // Set the token address value in formik state - setFieldValue(name, newToken.tokenCaip19Id); - // reset amount after change token - setFieldValue('amount', ''); - // Update local state - setToken(newToken); + setFieldValue(name, newToken); // Update nft state in parent - setIsNft(!!isNonFungibleToken(newToken.tokenCaip19Id)); + //TODO + setIsNft(false); }; const onClickField = () => { @@ -73,8 +51,7 @@ export function TokenSelectField({ return ( <> setIsModalOpen(false)} onSelect={onSelectToken} - originCaip2Id={originCaip2Id} - destinationCaip2Id={destinationCaip2Id} - tokenRoutes={tokenRoutes} + origin={origin} + destination={destination} /> ); @@ -93,13 +69,11 @@ export function TokenSelectField({ function TokenButton({ token, - name, disabled, onClick, isAutomatic, }: { - token?: TokenMetadata; - name: string; + token?: Token; disabled?: boolean; onClick?: () => void; isAutomatic?: boolean; @@ -107,7 +81,6 @@ function TokenButton({ return (
- + ); diff --git a/src/features/transfer/TransferTokenForm.tsx b/src/features/transfer/TransferTokenForm.tsx index bb5137c5..a8bccd86 100644 --- a/src/features/transfer/TransferTokenForm.tsx +++ b/src/features/transfer/TransferTokenForm.tsx @@ -1,9 +1,9 @@ -import BigNumber from 'bignumber.js'; import { Form, Formik, useFormikContext } from 'formik'; import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'react-toastify'; -import { fromWei, fromWeiRounded, toWei } from '@hyperlane-xyz/utils'; +import { TokenAmount } from '@hyperlane-xyz/sdk'; +import { ProtocolType, toWei } from '@hyperlane-xyz/utils'; import { SmallSpinner } from '../../components/animation/SmallSpinner'; import { ConnectAwareSubmitButton } from '../../components/buttons/ConnectAwareSubmitButton'; @@ -12,31 +12,30 @@ import { SolidButton } from '../../components/buttons/SolidButton'; import { ChevronIcon } from '../../components/icons/Chevron'; import { WideChevron } from '../../components/icons/WideChevron'; import { TextField } from '../../components/input/TextField'; +import { getTokens, getWarpCore } from '../../context/context'; import SwapIcon from '../../images/icons/swap.svg'; import { Color } from '../../styles/Color'; 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 { useAccountAddressForChain, useAccounts } from '../wallet/hooks/multiProtocol'; +import { + getAccountAddressForChain, + useAccountAddressForChain, + useAccounts, +} from '../wallet/hooks/multiProtocol'; +import { AccountInfo } from '../wallet/hooks/types'; import { TransferFormValues } from './types'; import { useIgpQuote } from './useIgpQuote'; import { useTokenTransfer } from './useTokenTransfer'; -import { validateFormValues } from './validateForm'; -export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { - const chainCaip2Ids = useRouteChains(tokenRoutes); - const initialValues = useFormInitialValues(chainCaip2Ids, tokenRoutes); +export function TransferTokenForm() { + const initialValues = useFormInitialValues(); const { accounts } = useAccounts(); // Flag for if form is in input vs review mode @@ -44,16 +43,10 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { // Flag for check current type of token const [isNft, setIsNft] = useState(false); - const { balances, igpQuote } = useStore((state) => ({ - balances: state.balances, - igpQuote: state.igpQuote, - })); - - const validate = (values: TransferFormValues) => - validateFormValues(values, tokenRoutes, balances, igpQuote, accounts); + const validate = (values: TransferFormValues) => validateForm(values, accounts); const onSubmitForm = (values: TransferFormValues) => { - logger.debug('Reviewing transfer form values:', JSON.stringify(values)); + logger.debug('Reviewing transfer form values for:', values.origin, values.destination); setIsReview(true); }; @@ -66,14 +59,14 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { validateOnBlur={false} >
- +
- - + +
- - - + + + ); @@ -81,16 +74,15 @@ export function TransferTokenForm({ tokenRoutes }: { tokenRoutes: RoutesMap }) { function SwapChainsButton({ disabled }: { disabled?: boolean }) { const { values, setFieldValue } = useFormikContext(); - const { originCaip2Id, destinationCaip2Id } = values; + const { origin, destination } = values; const onClick = () => { if (disabled) return; - setFieldValue('originCaip2Id', destinationCaip2Id); - setFieldValue('destinationCaip2Id', originCaip2Id); + setFieldValue('origin', destination); + setFieldValue('destination', origin); // Reset other fields on chain change + setFieldValue('token', undefined); setFieldValue('recipientAddress', ''); - setFieldValue('amount', ''); - setFieldValue('tokenCaip19Id', ''); }; return ( @@ -106,21 +98,12 @@ function SwapChainsButton({ disabled }: { disabled?: boolean }) { ); } -function ChainSelectSection({ - chainCaip2Ids, - isReview, -}: { - chainCaip2Ids: ChainCaip2Id[]; - isReview: boolean; -}) { +function ChainSelectSection({ isReview }: { isReview: boolean }) { + const chains = useMemo(() => getWarpCore().getTokenChains(), []); + return (
- +
@@ -129,22 +112,15 @@ function ChainSelectSection({
- +
); } function TokenSection({ - tokenRoutes, setIsNft, isReview, }: { - tokenRoutes: RoutesMap; setIsNft: (b: boolean) => void; isReview: boolean; }) { @@ -152,14 +128,13 @@ function TokenSection({ return (
-
); } -function RecipientSection({ - tokenRoutes, - isReview, -}: { - tokenRoutes: RoutesMap; - isReview: boolean; -}) { +function RecipientSection({ isReview }: { isReview: boolean }) { const { values } = useFormikContext(); - const { balance, decimals } = useDestinationBalance(values, tokenRoutes); + const { balance } = useDestinationBalance(values); // A crude way to detect transfer completions by triggering // toast on recipientAddress balance increase. This is not ideal because it // could confuse unrelated balance changes for message delivery // TODO replace with a polling worker that queries the hyperlane explorer const recipientAddress = values.recipientAddress; - const prevRecipientBalance = useRef<{ balance?: string; recipientAddress?: string }>({ - balance: '', + const prevRecipientBalance = useRef<{ balance?: TokenAmount; recipientAddress?: string }>({ recipientAddress: '', }); useEffect(() => { @@ -231,7 +191,8 @@ function RecipientSection({ balance && prevRecipientBalance.current.balance && prevRecipientBalance.current.recipientAddress === recipientAddress && - new BigNumber(balance).gt(prevRecipientBalance.current.balance) + balance.equals(prevRecipientBalance.current.balance) && + balance.amount > prevRecipientBalance.current.balance.amount ) { toast.success('Recipient has received funds, transfer complete!'); } @@ -244,7 +205,7 @@ function RecipientSection({ - +
{`${label}: ${value}`}
; } function ButtonSection({ - tokenRoutes, isReview, setIsReview, }: { - tokenRoutes: RoutesMap; isReview: boolean; setIsReview: (b: boolean) => void; }) { @@ -296,13 +247,13 @@ function ButtonSection({ const triggerTransactionsHandler = async () => { setTransferLoading(true); - await triggerTransactions(values, tokenRoutes); + await triggerTransactions(values); }; if (!isReview) { return ( @@ -326,24 +277,17 @@ function ButtonSection({ onClick={triggerTransactionsHandler} classes="flex-1 px-3 py-1.5" > - {`Send to ${getChainDisplayName(values.destinationCaip2Id)}`} + {`Send to ${getChainDisplayName(values.destination)}`}
); } -function MaxButton({ - balance, - decimals, - disabled, -}: { - balance?: string | null; - decimals?: number; - disabled?: boolean; -}) { +function MaxButton({ balance, disabled }: { balance?: TokenAmount; disabled?: boolean }) { const { setFieldValue } = useFormikContext(); const onClick = () => { - if (balance && !disabled) setFieldValue('amount', fromWeiRounded(balance, decimals)); + if (!balance || disabled) return; + setFieldValue('amount', balance.getDecimalFormattedAmount().toFixed(4)); }; return ( (); - const address = useAccountAddressForChain(values.destinationCaip2Id); + const address = useAccountAddressForChain(values.destination); const onClick = () => { if (disabled) return; if (address) setFieldValue('recipientAddress', address); else toast.warn( `No account found for for chain ${getChainDisplayName( - values.destinationCaip2Id, + values.destination, )}, is your wallet connected?`, ); }; @@ -384,31 +328,21 @@ function SelfButton({ disabled }: { disabled?: boolean }) { ); } -function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes: RoutesMap }) { +function ReviewDetails({ visible }: { visible: boolean }) { const { - values: { amount, originCaip2Id, destinationCaip2Id, tokenCaip19Id }, + values: { amount, destination, token }, } = useFormikContext(); - // TODO cosmos: Need better handling of IBC route type (remove cast) - const route = getTokenRoute( - originCaip2Id, - destinationCaip2Id, - tokenCaip19Id, - tokenRoutes, - ) as WarpRoute; - const isNft = tokenCaip19Id && isNonFungibleToken(tokenCaip19Id); - const amountWei = isNft ? amount.toString() : toWei(amount, route?.originDecimals); - const originToken = getToken(tokenCaip19Id); - const originTokenSymbol = originToken?.symbol || ''; + const isNft = token?.isNft(); + const amountWei = isNft ? amount.toString() : toWei(amount, token?.decimals); + const originTokenSymbol = token?.symbol || ''; const { isLoading: isApproveLoading, isApproveRequired } = useIsApproveRequired( - tokenCaip19Id, + token, amountWei, - route, visible, ); - const { isLoading: isQuoteLoading, igpQuote } = useIgpQuote(route); - const showIgpQuote = route && !isIbcOnlyRoute(route); + const { isLoading: isQuoteLoading, igpQuote } = useIgpQuote(token, destination); const isLoading = isApproveLoading || isQuoteLoading; @@ -429,9 +363,9 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes

Transaction 1: Approve Transfer

-

{`Token Address: ${getTokenAddress(tokenCaip19Id)}`}

- {route?.baseRouterAddress && ( -

{`Collateral Address: ${route.baseRouterAddress}`}

+

{`Router Address: ${token?.addressOrDenom}`}

+ {token?.collateralAddressOrDenom && ( +

{`Collateral Address: ${token.collateralAddressOrDenom}`}

)}
@@ -456,12 +390,10 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes Amount {`${amount} ${originTokenSymbol}`}

- {showIgpQuote && ( -

- Interchain Gas - {`${igpQuote?.amount || '0'} ${igpQuote?.token?.symbol || ''}`} -

- )} +

+ Interchain Gas + {`${igpQuote?.amount || '0'} ${igpQuote?.token?.symbol || ''}`} +

)}
@@ -472,20 +404,29 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes ); } -function useFormInitialValues( - chainCaip2Ids: ChainCaip2Id[], - tokenRoutes: RoutesMap, -): TransferFormValues { +function useFormInitialValues(): TransferFormValues { return useMemo(() => { - const firstRoute = Object.values(tokenRoutes[chainCaip2Ids[0]]).filter( - (routes) => routes.length, - )[0][0]; + const firstToken = getTokens().filter((t) => t.connectedTokens?.length)[0]; + const connectedToken = firstToken.connectedTokens![0]; return { - originCaip2Id: firstRoute.originCaip2Id, - destinationCaip2Id: firstRoute.destCaip2Id, + origin: firstToken.chainName, + destination: connectedToken.chainName, + token: firstToken, amount: '', - tokenCaip19Id: firstRoute.baseTokenCaip19Id, recipientAddress: '', }; - }, [chainCaip2Ids, tokenRoutes]); + }, []); +} + +function validateForm(values: TransferFormValues, accounts: Record) { + const { origin, destination, token, amount, recipientAddress } = values; + if (!token) return { token: 'Token is required' }; + const amountWei = toWei(amount, token.decimals); + const sender = getAccountAddressForChain(origin, accounts) || ''; + return getWarpCore().validateTransfer( + token.amount(amountWei), + destination, + sender, + recipientAddress, + ); } diff --git a/src/features/transfer/TransfersDetailsModal.tsx b/src/features/transfer/TransfersDetailsModal.tsx index 629b65e0..55f93a07 100644 --- a/src/features/transfer/TransfersDetailsModal.tsx +++ b/src/features/transfer/TransfersDetailsModal.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { isZeroishAddress, toTitleCase } from '@hyperlane-xyz/utils'; +import { toTitleCase } from '@hyperlane-xyz/utils'; import { MessageStatus, MessageTimeline, useMessageTimeline } from '@hyperlane-xyz/widgets'; import { Spinner } from '../../components/animation/Spinner'; @@ -10,6 +10,7 @@ import { ChainLogo } from '../../components/icons/ChainLogo'; import { TokenIcon } from '../../components/icons/TokenIcon'; import { WideChevron } from '../../components/icons/WideChevron'; import { Modal } from '../../components/layout/Modal'; +import { getMultiProvider } from '../../context/context'; import LinkIcon from '../../images/icons/external-link-icon.svg'; import { formatTimestamp } from '../../utils/date'; import { getHypExplorerLink } from '../../utils/links'; @@ -21,11 +22,7 @@ import { isTransferFailed, isTransferSent, } from '../../utils/transfer'; -import { getChainReference } from '../caip/chains'; -import { AssetNamespace, parseCaip19Id } from '../caip/tokens'; import { getChainDisplayName, hasPermissionlessChain } from '../chains/utils'; -import { getMultiProvider } from '../multiProvider'; -import { getToken } from '../tokens/metadata'; import { useAccountForChain } from '../wallet/hooks/multiProtocol'; import { TransferContext, TransferStatus } from './types'; @@ -44,15 +41,11 @@ export function TransfersDetailsModal({ const [originTxUrl, setOriginTxUrl] = useState(''); const { params, status, originTxHash, msgId, timestamp, activeAccountAddress } = transfer || {}; - const { destinationCaip2Id, originCaip2Id, tokenCaip19Id, amount, recipientAddress } = - params || {}; + // TODO stored value can't have whole token + const { destination, origin, token, amount, recipientAddress } = params || {}; - const account = useAccountForChain(originCaip2Id); + const account = useAccountForChain(origin); const multiProvider = getMultiProvider(); - const originChain = getChainReference(originCaip2Id); - const destChain = getChainReference(destinationCaip2Id); - const { address: tokenAddress, namespace: tokenNamespace } = parseCaip19Id(tokenCaip19Id); - const isNative = tokenNamespace === AssetNamespace.native || isZeroishAddress(tokenAddress); const getMessageUrls = useCallback(async () => { try { @@ -80,9 +73,9 @@ export function TransfersDetailsModal({ const isAccountReady = !!account?.isReady; const connectorName = account?.connectorName || 'wallet'; - const token = getToken(tokenCaip19Id); + const token = getToken(token); - const isPermissionlessRoute = hasPermissionlessChain([destinationCaip2Id, originCaip2Id]); + const isPermissionlessRoute = hasPermissionlessChain([destination, origin]); const isSent = isTransferSent(status); const isFailed = isTransferFailed(status); @@ -100,7 +93,7 @@ export function TransfersDetailsModal({ [timestamp], ); - const explorerLink = getHypExplorerLink(originCaip2Id, msgId); + const explorerLink = getHypExplorerLink(origin, msgId); return (
- + - {getChainDisplayName(originCaip2Id, true)} + {getChainDisplayName(origin, true)}
@@ -152,9 +145,9 @@ export function TransfersDetailsModal({
- + - {getChainDisplayName(destinationCaip2Id, true)} + {getChainDisplayName(destination, true)}
diff --git a/src/features/transfer/types.ts b/src/features/transfer/types.ts index 38d6b866..c2cb006a 100644 --- a/src/features/transfer/types.ts +++ b/src/features/transfer/types.ts @@ -1,9 +1,9 @@ -import type { Route } from '../routes/types'; +import { Token } from '@hyperlane-xyz/sdk'; export interface TransferFormValues { - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - tokenCaip19Id: TokenCaip19Id; + origin: ChainName; + destination: ChainName; + token: Token | undefined; amount: string; recipientAddress: Address; } @@ -35,23 +35,3 @@ export interface TransferContext { timestamp: number; activeAccountAddress: Address; } - -export enum IgpTokenType { - NativeSeparate = 'native-separate', // Paying with origin chain native token - NativeCombined = 'native-combined', // Both igp fees and transfer token are native - TokenSeparate = 'token-separate', // Paying with a different non-native token - TokenCombined = 'token-combined', // Paying with the same token being transferred -} - -export interface IgpQuote { - type: IgpTokenType; - amount: string; - weiAmount: string; - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - token: { - tokenCaip19Id: TokenCaip19Id; - symbol: string; - decimals: number; - }; -} diff --git a/src/features/transfer/useIgpQuote.ts b/src/features/transfer/useIgpQuote.ts index feebe80e..f6416497 100644 --- a/src/features/transfer/useIgpQuote.ts +++ b/src/features/transfer/useIgpQuote.ts @@ -1,118 +1,20 @@ import { useQuery } from '@tanstack/react-query'; -import { useEffect } from 'react'; -import { IHypTokenAdapter } from '@hyperlane-xyz/sdk'; -import { ProtocolType, fromWei, isAddress } from '@hyperlane-xyz/utils'; +import { Token } from '@hyperlane-xyz/sdk'; import { useToastError } from '../../components/toast/useToastError'; -import { DEFAULT_IGP_QUOTES } from '../../consts/igpQuotes'; -import { getChainReference, parseCaip2Id } from '../caip/chains'; -import { AssetNamespace, getCaip19Id, getNativeTokenAddress } from '../caip/tokens'; -import { getChainMetadata, getMultiProvider } from '../multiProvider'; -import { Route } from '../routes/types'; -import { - isIbcOnlyRoute, - isIbcToWarpRoute, - isRouteFromCollateral, - isRouteFromNative, -} from '../routes/utils'; -import { useStore } from '../store'; -import { AdapterFactory } from '../tokens/AdapterFactory'; -import { findTokensByAddress, getToken } from '../tokens/metadata'; - -import { IgpQuote, IgpTokenType } from './types'; - -export function useIgpQuote(route?: Route) { - const setIgpQuote = useStore((state) => state.setIgpQuote); +import { getWarpCore } from '../../context/context'; +export function useIgpQuote(token?: Token, destination?: ChainName) { const { isLoading, isError, error, data } = useQuery({ - queryKey: ['useIgpQuote', route], + queryKey: ['useIgpQuote', token, destination], queryFn: () => { - if (!route || isIbcOnlyRoute(route)) return null; - return fetchIgpQuote(route); + if (!token || !destination) return null; + return getWarpCore().getTransferGasQuote(token, destination); }, }); - useEffect(() => { - setIgpQuote(data || null); - }, [data, setIgpQuote]); - useToastError(error, 'Error fetching IGP quote'); return { isLoading, isError, igpQuote: data }; } - -export async function fetchIgpQuote(route: Route, adapter?: IHypTokenAdapter): Promise { - const { baseTokenCaip19Id, originCaip2Id, destCaip2Id: destinationCaip2Id } = route; - const { protocol: originProtocol, reference: originChainId } = parseCaip2Id(originCaip2Id); - const baseToken = getToken(baseTokenCaip19Id); - if (!baseToken) throw new Error(`No base token found for ${baseTokenCaip19Id}`); - - let weiAmount: string; - const defaultQuotes = DEFAULT_IGP_QUOTES[originProtocol]; - if (typeof defaultQuotes === 'string') { - weiAmount = defaultQuotes; - } else if (defaultQuotes?.[originChainId]) { - weiAmount = defaultQuotes[originChainId]; - } else { - // Otherwise, compute IGP quote via the adapter - adapter ||= AdapterFactory.HypTokenAdapterFromRouteOrigin(route); - const destinationChainId = getChainReference(destinationCaip2Id); - const destinationDomainId = getMultiProvider().getDomainId(destinationChainId); - weiAmount = await adapter.quoteGasPayment(destinationDomainId); - } - - // Determine the IGP token - const isRouteFromBase = isRouteFromCollateral(route) || isIbcToWarpRoute(route); - let type: IgpTokenType; - let tokenCaip19Id: TokenCaip19Id; - let tokenSymbol: string; - let tokenDecimals: number; - // If the token has an explicit IGP token address set, use that - // Custom igpTokenAddress configs are supported only from the base (i.e. collateral) token is supported atm - if ( - isRouteFromBase && - baseToken.igpTokenAddressOrDenom && - isAddress(baseToken.igpTokenAddressOrDenom) - ) { - type = IgpTokenType.TokenSeparate; - const igpToken = findTokensByAddress(baseToken.igpTokenAddressOrDenom)[0]; - tokenCaip19Id = igpToken.tokenCaip19Id; - // Note this assumes the u prefix because only cosmos tokens use this case - tokenSymbol = igpToken.symbol; - tokenDecimals = igpToken.decimals; - } else if (originProtocol === ProtocolType.Cosmos) { - // TODO Handle case of an evm-based token warped to cosmos - if (!isRouteFromBase) throw new Error('IGP quote for cosmos synthetics not yet supported'); - // If the protocol is cosmos, use the base token - type = IgpTokenType.TokenCombined; - tokenCaip19Id = baseToken.tokenCaip19Id; - tokenSymbol = baseToken.symbol; - tokenDecimals = baseToken.decimals; - } else { - // Otherwise use the plain old native token from the route origin - type = isRouteFromNative(route) ? IgpTokenType.NativeCombined : IgpTokenType.NativeSeparate; - const originNativeToken = getChainMetadata(originCaip2Id).nativeToken; - if (!originNativeToken) throw new Error(`No native token for ${originCaip2Id}`); - tokenCaip19Id = getCaip19Id( - originCaip2Id, - AssetNamespace.native, - getNativeTokenAddress(originProtocol), - ); - tokenSymbol = originNativeToken.symbol; - tokenDecimals = originNativeToken.decimals; - } - - return { - type, - amount: fromWei(weiAmount, tokenDecimals), - weiAmount, - originCaip2Id, - destinationCaip2Id, - token: { - tokenCaip19Id, - symbol: tokenSymbol, - decimals: tokenDecimals, - }, - }; -} diff --git a/src/features/transfer/useTokenTransfer.ts b/src/features/transfer/useTokenTransfer.ts index b1668795..b72eddda 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -120,7 +120,13 @@ async function executeTransfer({ let status: TransferStatus = TransferStatus.Preparing; try { - const { originCaip2Id, destinationCaip2Id, tokenCaip19Id, amount, recipientAddress } = values; + const { + origin: originCaip2Id, + destination: destinationCaip2Id, + token: tokenCaip19Id, + amount, + recipientAddress, + } = values; const { protocol: originProtocol } = parseCaip2Id(originCaip2Id); const { reference: destReference } = parseCaip2Id(destinationCaip2Id); const destinationDomainId = getMultiProvider().getDomainId(destReference); diff --git a/src/features/transfer/validateForm.ts b/src/features/transfer/validateForm.ts deleted file mode 100644 index b7c5d0a9..00000000 --- a/src/features/transfer/validateForm.ts +++ /dev/null @@ -1,223 +0,0 @@ -import BigNumber from 'bignumber.js'; -import { toast } from 'react-toastify'; - -import { - ProtocolType, - isValidAddress, - isZeroishAddress, - toWei, - tryParseAmount, -} from '@hyperlane-xyz/utils'; - -import { toastIgpDetails } from '../../components/toast/IgpDetailsToast'; -import { config } from '../../consts/config'; -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 { getAccountAddressForChain } from '../wallet/hooks/multiProtocol'; -import { AccountInfo } from '../wallet/hooks/types'; - -import { IgpQuote, IgpTokenType, TransferFormValues } from './types'; - -type FormError = Partial>; -type Balances = AppState['balances']; - -export async function validateFormValues( - values: TransferFormValues, - tokenRoutes: RoutesMap, - balances: Balances, - igpQuote: IgpQuote | null, - accounts: Record, -): Promise { - const { originCaip2Id, destinationCaip2Id, amount, tokenCaip19Id, recipientAddress } = values; - const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); - if (!route) return { destinationCaip2Id: 'No route found for chains/token' }; - - const chainError = validateChains(originCaip2Id, destinationCaip2Id); - if (chainError) return chainError; - - const tokenError = validateToken(tokenCaip19Id); - if (tokenError) return tokenError; - - const recipientError = validateRecipient(recipientAddress, destinationCaip2Id); - if (recipientError) return recipientError; - - const isNft = isNonFungibleToken(tokenCaip19Id); - - const { error: amountError, parsedAmount } = validateAmount(amount, isNft); - if (amountError) return amountError; - - if (isNft) { - const balancesError = validateNftBalances(balances, parsedAmount.toString()); - if (balancesError) return balancesError; - } else { - const balancesError = await validateTokenBalances({ - balances, - parsedAmount, - route, - igpQuote, - accounts, - }); - if (balancesError) return balancesError; - } - - return {}; -} - -function validateChains( - originCaip2Id: ChainCaip2Id, - destinationCaip2Id: ChainCaip2Id, -): FormError | null { - if (!originCaip2Id) return { originCaip2Id: 'Invalid origin chain' }; - if (!destinationCaip2Id) return { destinationCaip2Id: 'Invalid destination chain' }; - if ( - config.withdrawalWhitelist && - !config.withdrawalWhitelist.split(',').includes(destinationCaip2Id) - ) { - return { destinationCaip2Id: 'Bridge is in deposit-only mode' }; - } - if ( - config.transferBlacklist && - config.transferBlacklist.split(',').includes(`${originCaip2Id}-${destinationCaip2Id}`) - ) { - return { destinationCaip2Id: 'Route is not currently allowed' }; - } - return null; -} - -function validateToken(tokenCaip19Id: TokenCaip19Id): FormError | null { - if (!tokenCaip19Id) return { tokenCaip19Id: 'Token required' }; - const { address: tokenAddress } = parseCaip19Id(tokenCaip19Id); - const tokenMetadata = getToken(tokenCaip19Id); - if (!tokenMetadata || (!isZeroishAddress(tokenAddress) && !isValidAddress(tokenAddress))) { - return { tokenCaip19Id: 'Invalid token' }; - } - return null; -} - -function validateRecipient( - recipientAddress: Address, - destinationCaip2Id: ChainCaip2Id, -): FormError | null { - const destProtocol = getProtocolType(destinationCaip2Id); - // Ensure recip address is valid for the destination chain's protocol - if (!isValidAddress(recipientAddress, destProtocol)) - return { recipientAddress: 'Invalid recipient' }; - // Also ensure the address denom is correct if the dest protocol is Cosmos - if (destProtocol === ProtocolType.Cosmos) { - const destChainPrefix = getChainMetadata(destinationCaip2Id).bech32Prefix; - if (!destChainPrefix) { - toast.error(`No bech32 prefix found for chain ${destinationCaip2Id}`); - return { destinationCaip2Id: 'Invalid chain data' }; - } else if (!recipientAddress.startsWith(destChainPrefix)) { - toast.error(`Recipient address prefix should be ${destChainPrefix}`); - return { recipientAddress: `Invalid recipient prefix` }; - } - } - return null; -} - -function validateAmount( - amount: string, - isNft: boolean, -): { parsedAmount: BigNumber; error: FormError | null } { - const parsedAmount = tryParseAmount(amount); - if (!parsedAmount || parsedAmount.lte(0)) { - return { - parsedAmount: BigNumber(0), - error: { amount: isNft ? 'Invalid Token Id' : 'Invalid amount' }, - }; - } - return { parsedAmount, error: null }; -} - -// Validate balances for ERC721-like tokens -function validateNftBalances(balances: Balances, nftId: string | number): FormError | null { - const { isSenderNftOwner, senderNftIds } = balances; - if (isSenderNftOwner === false || (senderNftIds && !senderNftIds.includes(nftId.toString()))) { - return { amount: 'Token ID not owned' }; - } - return null; -} - -// Validate balances for ERC20-like tokens -async function validateTokenBalances({ - balances, - parsedAmount, - route, - igpQuote, - accounts, -}: { - balances: Balances; - parsedAmount: BigNumber; - route: Route; - igpQuote: IgpQuote | null; - accounts: Record; -}): Promise { - const sendValue = new BigNumber(toWei(parsedAmount, route.originDecimals)); - - // First check basic token balance - if (sendValue.gt(balances.senderTokenBalance)) return { amount: 'Insufficient balance' }; - - // Next, ensure balances can cover IGP fees - // But not for pure IBC routes because IGP is not used - if (isIbcOnlyRoute(route)) return null; - - if (!igpQuote?.weiAmount) return { amount: 'Interchain gas quote not ready' }; - const { type: igpTokenType, amount: igpAmount, weiAmount: igpWeiAmount } = igpQuote; - const { symbol: igpTokenSymbol, tokenCaip19Id: igpTokenCaip19Id } = igpQuote.token; - - let igpTokenBalance: string; - if ([IgpTokenType.NativeCombined, IgpTokenType.NativeSeparate].includes(igpTokenType)) { - igpTokenBalance = balances.senderNativeBalance; - } else if (igpTokenType === IgpTokenType.TokenCombined) { - igpTokenBalance = balances.senderTokenBalance; - } else if (igpTokenType === IgpTokenType.TokenSeparate) { - igpTokenBalance = await fetchSenderTokenBalance( - accounts, - route.originCaip2Id, - igpTokenCaip19Id, - ); - } else { - return { amount: 'Interchain gas quote not valid' }; - } - - const requiredIgpTokenBalance = [ - IgpTokenType.NativeCombined, - IgpTokenType.TokenCombined, - ].includes(igpTokenType) - ? sendValue.plus(igpWeiAmount) - : BigNumber(igpWeiAmount); - - if (requiredIgpTokenBalance.gt(igpTokenBalance)) { - toastIgpDetails(igpAmount, igpTokenSymbol); - return { amount: `Insufficient ${igpTokenSymbol} for gas` }; - } - - return null; -} - -async function fetchSenderTokenBalance( - accounts: Record, - originCaip2Id: ChainCaip2Id, - igpTokenCaip19Id: TokenCaip19Id, -) { - try { - const account = accounts[getProtocolType(originCaip2Id)]; - const sender = getAccountAddressForChain(originCaip2Id, account); - if (!sender) throw new Error('No sender address found'); - const adapter = AdapterFactory.TokenAdapterFromAddress(igpTokenCaip19Id); - const igpTokenBalance = await adapter.getBalance(sender); - return igpTokenBalance; - } catch (error) { - logger.error('Error fetching token balance during form validation', error); - toast.error('Error fetching balance for validation'); - throw error; - } -} diff --git a/src/features/wallet/SideBarMenu.tsx b/src/features/wallet/SideBarMenu.tsx index d9ef9d73..3c93363b 100644 --- a/src/features/wallet/SideBarMenu.tsx +++ b/src/features/wallet/SideBarMenu.tsx @@ -146,7 +146,7 @@ export function SideBarMenu({ >
- +
@@ -155,15 +155,15 @@ export function SideBarMenu({ {t.params.amount} - {getToken(t.params.tokenCaip19Id)?.symbol || ''} + {getToken(t.params.token)?.symbol || ''} - ({toTitleCase(getAssetNamespace(t.params.tokenCaip19Id))}) + ({toTitleCase(getAssetNamespace(t.params.token))})
- {getChainDisplayName(t.params.originCaip2Id, true)} + {getChainDisplayName(t.params.origin, true)} - {getChainDisplayName(t.params.destinationCaip2Id, true)} + {getChainDisplayName(t.params.destination, true)}
diff --git a/src/features/wallet/context/EvmWalletContext.tsx b/src/features/wallet/context/EvmWalletContext.tsx index 5fad4e37..87916f35 100644 --- a/src/features/wallet/context/EvmWalletContext.tsx +++ b/src/features/wallet/context/EvmWalletContext.tsx @@ -19,10 +19,10 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; import { APP_NAME } from '../../../consts/app'; import { config } from '../../../consts/config'; -import { tokenList } from '../../../consts/tokens'; +import { getWarpCore } from '../../../context/context'; import { Color } from '../../../styles/Color'; import { getWagmiChainConfig } from '../../chains/metadata'; -import { getMultiProvider } from '../../multiProvider'; +import { tryGetChainMetadata } from '../../chains/utils'; const { chains, publicClient } = configureChains(getWagmiChainConfig(), [publicProvider()]); @@ -63,11 +63,9 @@ const wagmiConfig = createConfig({ export function EvmWalletContext({ children }: PropsWithChildren) { const initialChain = useMemo(() => { - const multiProvider = getMultiProvider(); - return tokenList.filter( - (token) => - multiProvider.tryGetChainMetadata(token.chainId)?.protocol === ProtocolType.Ethereum, - )?.[0]?.chainId as number; + const tokens = getWarpCore().tokens; + const firstEvmToken = tokens.filter((token) => token.protocol === ProtocolType.Ethereum)?.[0]; + return tryGetChainMetadata(firstEvmToken?.chainName)?.chainId as number; }, []); return ( diff --git a/src/features/wallet/hooks/multiProtocol.tsx b/src/features/wallet/hooks/multiProtocol.tsx index 829ecfc9..a476680e 100644 --- a/src/features/wallet/hooks/multiProtocol.tsx +++ b/src/features/wallet/hooks/multiProtocol.tsx @@ -6,6 +6,7 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; import { config } from '../../../consts/config'; import { logger } from '../../../utils/logger'; import { tryGetProtocolType } from '../../caip/chains'; +import { getChainProtocol } from '../../chains/utils'; import { useCosmosAccount, @@ -80,12 +81,14 @@ export function useAccountAddressForChain(chainCaip2Id?: ChainCaip2Id): Address } export function getAccountAddressForChain( - chainCaip2Id?: ChainCaip2Id, - account?: AccountInfo, + chainName?: ChainName, + accounts?: Record, ): Address | undefined { - if (!chainCaip2Id || !account?.addresses.length) return undefined; - if (account.protocol === ProtocolType.Cosmos) { - return account.addresses.find((a) => a.chainCaip2Id === chainCaip2Id)?.address; + if (!chainName || !accounts) return undefined; + const protocol = getChainProtocol(chainName); + const account = accounts[protocol]; + if (protocol === ProtocolType.Cosmos) { + return account.addresses.find((a) => a.chainName === chainName)?.address; } else { // Use first because only cosmos has the notion of per-chain addresses return account.addresses[0].address; diff --git a/src/global.d.ts b/src/global.d.ts index b1195eff..74fc0ac9 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,8 +1,7 @@ declare type Address = string; +declare type ChainName = string; declare type ChainId = number | string; declare type DomainId = number; -declare type ChainCaip2Id = `${string}:${string}`; // e.g. ethereum:1 or sealevel:1399811149 -declare type TokenCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f declare module '*.yaml' { const data: any; diff --git a/src/utils/zod.ts b/src/utils/zod.ts new file mode 100644 index 00000000..8c612364 --- /dev/null +++ b/src/utils/zod.ts @@ -0,0 +1,15 @@ +import { SafeParseReturnType } from 'zod'; + +import { logger } from './logger'; + +export function validateZodResult( + result: SafeParseReturnType, + desc: string = 'config', +): T { + if (!result.success) { + logger.warn(`Invalid ${desc}`, result.error); + throw new Error(`Invalid desc: ${result.error.toString()}`); + } else { + return result.data; + } +}