diff --git a/.eslintrc b/.eslintrc index e4e5a50e..dcc1fd5e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,6 +29,14 @@ "@typescript-eslint/no-explicit-any": ["off"], "@typescript-eslint/no-non-null-assertion": ["off"], "@typescript-eslint/no-require-imports": ["warn"], + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], "jsx-a11y/alt-text": ["off"] } } 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 37949aab..6619c3ae 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/warp-ui-template", "description": "A web app template for building Hyperlane Warp Route UIs", - "version": "3.6.1", + "version": "3.8.0", "author": "J M Rossy", "dependencies": { "@chakra-ui/next-js": "^2.1.5", @@ -16,9 +16,9 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.14", - "@hyperlane-xyz/sdk": "^3.6.1", - "@hyperlane-xyz/utils": "^3.6.1", - "@hyperlane-xyz/widgets": "^3.1.4", + "@hyperlane-xyz/sdk": "^3.8.0", + "@hyperlane-xyz/utils": "^3.8.0", + "@hyperlane-xyz/widgets": "^3.8.0", "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6", "@rainbow-me/rainbowkit": "1.3.0", "@sentry/nextjs": "^7.93.0", @@ -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 --passWithNoTests", "prettier": "prettier --write ./src" }, "types": "dist/src/index.d.ts", @@ -95,6 +94,7 @@ "cosmjs-types": "0.9", "ethers": "^5.7", "lit-html": "2.8.0", + "react-fast-compare": "^3.2", "viem": "1.20.0", "zustand": "^4.4" } diff --git a/src/components/animation/SmallSpinner.tsx b/src/components/animation/SmallSpinner.tsx index aae0e1b3..af4a81d3 100644 --- a/src/components/animation/SmallSpinner.tsx +++ b/src/components/animation/SmallSpinner.tsx @@ -1,9 +1,9 @@ import { memo } from 'react'; -function _SmallSpinner() { +function _SmallSpinner({ className }: { className?: string }) { return ( ({ 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..87a9cdcd 100644 --- a/src/components/icons/ChainLogo.tsx +++ b/src/components/icons/ChainLogo.tsx @@ -1,42 +1,29 @@ 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 chainMetadata = tryGetChainMetadata(chainName); + const chainId = chainMetadata?.chainId; + const logoUri = chainMetadata?.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..5713a8bd 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 { IToken } 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?: IToken | null; 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/chains.ts b/src/consts/chains.ts index ac44de59..7c199cc8 100644 --- a/src/consts/chains.ts +++ b/src/consts/chains.ts @@ -1,63 +1,7 @@ -import { ChainMap, ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk'; // A map of chain names to ChainMetadata // Chains can be defined here, in chains.json, or in chains.yaml // Chains already in the SDK need not be included here unless you want to override some fields // Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts -export const chains: ChainMap = { - inevm: { - blockExplorers: [ - { - apiUrl: 'https://inevm.calderaexplorer.xyz/api', - family: ExplorerFamily.Blockscout, - name: 'Caldera inEVM Explorer', - url: 'https://inevm.calderaexplorer.xyz', - }, - ], - blocks: { - confirmations: 1, - estimateBlockTime: 3, - reorgPeriod: 0, - }, - chainId: 2525, - domainId: 2525, - displayName: 'Injective EVM', - displayNameShort: 'inEVM', - name: 'inevm', - nativeToken: { - decimals: 18, - name: 'Injective', - symbol: 'INJ', - }, - protocol: ProtocolType.Ethereum, - rpcUrls: [{ http: 'https://inevm.calderachain.xyz/http' }], - logoURI: '/logos/inevm.svg', - }, - - injective: { - blockExplorers: [], - blocks: { - confirmations: 1, - estimateBlockTime: 3, - reorgPeriod: 1, - }, - chainId: 'injective-1', - domainId: 6909546, - displayName: 'Injective', - displayNameShort: 'Injective', - name: 'injective', - nativeToken: { - decimals: 18, - name: 'Injective', - symbol: 'INJ', - }, - protocol: ProtocolType.Cosmos, - slip44: 118, - bech32Prefix: 'inj', - grpcUrls: [{ http: 'sentry.chain.grpc.injective.network:443' }], - rpcUrls: [{ http: 'https://sentry.tm.injective.network:443' }], - restUrls: [{ http: ' https://sentry.lcd.injective.network:443' }], - logoURI: '/logos/injective.svg', - }, -}; +export const chains: ChainMap = {}; diff --git a/src/consts/ibcRoutes.ts b/src/consts/ibcRoutes.ts deleted file mode 100644 index b5d3fe2e..00000000 --- a/src/consts/ibcRoutes.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { IbcRoute, IbcToWarpRoute } from '../features/routes/types'; - -// Configs for manually-defined IBC-only routes -export const ibcRoutes: Array = []; diff --git a/src/consts/igpQuotes.ts b/src/consts/igpQuotes.ts deleted file mode 100644 index fa522133..00000000 --- a/src/consts/igpQuotes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ProtocolType } from '@hyperlane-xyz/utils'; - -// IGP Quote overrides can be set here -// If specified, this value will be used instead of querying the token adapter -// Protocol to value | map -export const DEFAULT_IGP_QUOTES: Partial< - Record> -> = { - [ProtocolType.Sealevel]: '10000', - [ProtocolType.Cosmos]: { - celestia: '270000', // 0.27 TIA - 'neutron-1': '270000', // 0.27 TIA - 'injective-1': '30000000000000000', // 0.03 INJ - }, -}; diff --git a/src/consts/tokens.json b/src/consts/tokens.json index fe51488c..f7a64427 100644 --- a/src/consts/tokens.json +++ b/src/consts/tokens.json @@ -1 +1,4 @@ -[] +{ + "tokens": [], + "options": {} +} diff --git a/src/consts/tokens.ts b/src/consts/tokens.ts index f0205b81..9490c8c6 100644 --- a/src/consts/tokens.ts +++ b/src/consts/tokens.ts @@ -1,53 +1,100 @@ -import { WarpTokenConfig } from '../features/tokens/types'; +import { Chains, TokenStandard, 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 = [ - // INJ on Injective - { - type: 'native', - chainId: 'injective-1', - name: 'Injective Coin', - symbol: 'INJ', - decimals: 18, - hypNativeAddress: 'inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k', - igpTokenAddressOrDenom: 'inj', - logoURI: '/logos/injective.svg', - }, +export const tokenConfigs: WarpCoreConfig = { + tokens: [ + // INJ on Injective to inEVM + { + chainName: Chains.injective, + standard: TokenStandard.CwHypNative, + name: 'Injective Coin', + symbol: 'INJ', + decimals: 18, + addressOrDenom: 'inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k', + logoURI: '/logos/injective.svg', + connections: [{ token: 'ethereum|inevm|0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4' }], + }, - // INJ on inEVM - { - type: 'native', - chainId: 2525, - name: 'Injective Coin', - symbol: 'INJ', - decimals: 18, - hypNativeAddress: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4', - logoURI: '/logos/injective.svg', - }, + // INJ on inEVM from Injective + { + chainName: Chains.inevm, + standard: TokenStandard.EvmHypNative, + name: 'Injective Coin', + symbol: 'INJ', + decimals: 18, + addressOrDenom: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4', + logoURI: '/logos/injective.svg', + connections: [{ token: 'cosmos|injective|inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k' }], + }, - // Injective USDC - { - type: 'collateral', - chainId: 1, - name: 'USDC', - symbol: 'USDC', - decimals: 6, - address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', - hypCollateralAddress: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f', - logoURI: '/logos/usdc.svg', - }, + // USDC on Ethereum to inEVM + { + chainName: Chains.ethereum, + standard: TokenStandard.EvmHypCollateral, + name: 'USDC', + symbol: 'USDC', + decimals: 6, + addressOrDenom: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f', + collateralAddressOrDenom: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + logoURI: '/logos/usdc.svg', + connections: [{ token: 'ethereum|inevm|0x8358d8291e3bedb04804975eea0fe9fe0fafb147' }], + }, + + // USDC on inEVM from Ethereum + { + chainName: Chains.inevm, + standard: TokenStandard.EvmHypSynthetic, + name: 'USDC', + symbol: 'USDC', + decimals: 6, + addressOrDenom: '0x8358d8291e3bedb04804975eea0fe9fe0fafb147', + logoURI: '/logos/usdc.svg', + connections: [{ token: 'ethereum|ethereum|0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f' }], + }, + + // USDT on Ethereum to inEVM + { + chainName: Chains.ethereum, + standard: TokenStandard.EvmHypCollateral, + name: 'USDT', + symbol: 'USDT', + decimals: 6, + addressOrDenom: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19', + collateralAddressOrDenom: '0xdAC17F958D2ee523a2206206994597C13D831ec7', + logoURI: '/logos/usdt.svg', + connections: [{ token: 'ethereum|inevm|0x97423a68bae94b5de52d767a17abcc54c157c0e5' }], + }, - // Injective USDT - { - type: 'collateral', - chainId: 1, - name: 'USDT', - symbol: 'USDT', - decimals: 6, - address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', - hypCollateralAddress: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19', - logoURI: '/logos/usdt.svg', + // USDT on inEVM to Ethereum + { + chainName: Chains.inevm, + standard: TokenStandard.EvmHypSynthetic, + name: 'USDT', + symbol: 'USDT', + decimals: 6, + addressOrDenom: '0x97423a68bae94b5de52d767a17abcc54c157c0e5', + logoURI: '/logos/usdt.svg', + connections: [{ token: 'ethereum|ethereum|0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19' }], + }, + ], + options: { + interchainFeeConstants: [ + { + origin: Chains.injective, + destination: Chains.inevm, + amount: '30000000000000000', // 0.03 INJ + addressOrDenom: 'inj', + }, + ], + localFeeConstants: [ + { + origin: Chains.injective, + destination: Chains.inevm, + amount: '1000000000000000', // 0.001 INJ + addressOrDenom: 'inj', + }, + ], }, -]; +}; diff --git a/src/consts/tokens.yaml b/src/consts/tokens.yaml index e0229f6d..2e6b1d4b 100644 --- a/src/consts/tokens.yaml +++ b/src/consts/tokens.yaml @@ -1,14 +1,5 @@ -# 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: [] 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/scripts/buildConfigs/chains.ts b/src/context/chains.ts similarity index 64% rename from src/scripts/buildConfigs/chains.ts rename to src/context/chains.ts index a3be0e2b..bced7e29 100644 --- a/src/scripts/buildConfigs/chains.ts +++ b/src/context/chains.ts @@ -1,22 +1,18 @@ -import path from 'path'; import { z } from 'zod'; import { ChainMap, ChainMetadata, ChainMetadataSchema, chainMetadata } from '@hyperlane-xyz/sdk'; -import ChainsJson from '../../consts/chains.json'; -import { chains as ChainsTS } from '../../consts/chains.ts'; -import { cosmosDefaultChain } from '../../features/chains/cosmosDefault'; -import { logger } from '../../utils/logger'; - -import { readYaml } from './utils'; +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 getProcessedChainConfigs() { - const ChainsYaml = readYaml(path.resolve(__dirname, '../../consts/chains.yaml')); - +export function getChainConfigs() { // Chains must include a cosmos chain or CosmosKit throws errors const result = ChainConfigSchema.safeParse({ cosmoshub: cosmosDefaultChain, diff --git a/src/context/context.ts b/src/context/context.ts index 1ce378f2..6a035808 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -1,29 +1,27 @@ -import { ChainMap, ChainMetadata, MultiProtocolProvider } from '@hyperlane-xyz/sdk'; +import { + ChainMap, + ChainMetadata, + IToken, + MultiProtocolProvider, + Token, + WarpCore, +} from '@hyperlane-xyz/sdk'; +import { isNullish } from '@hyperlane-xyz/utils'; -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 +29,36 @@ 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; +} + +export function getTokenByIndex(tokenIndex?: number) { + const tokens = getTokens(); + if (isNullish(tokenIndex) || tokenIndex >= tokens.length) return undefined; + return tokens[tokenIndex]; +} + +export function getIndexForToken(token?: IToken): number | undefined { + if (!token) return undefined; + const index = getTokens().indexOf(token as Token); + if (index >= 0) return index; + else return undefined; +} diff --git a/src/context/tokens.ts b/src/context/tokens.ts new file mode 100644 index 00000000..ecc71a0f --- /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 = [...configTs.tokens, ...configJson.tokens, ...configYaml.tokens]; + const options = { ...configTs.options, ...configJson.options, ...configYaml.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..ef65449b 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('recipient', ''); setFieldValue('amount', ''); - setFieldValue('tokenCaip19Id', ''); + setFieldValue('tokenIndex', undefined); 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/cosmosDefault.ts b/src/features/chains/cosmosDefault.ts index 03906fe1..0c493cee 100644 --- a/src/features/chains/cosmosDefault.ts +++ b/src/features/chains/cosmosDefault.ts @@ -16,6 +16,7 @@ export const cosmosDefaultChain: ChainMetadata = { name: 'Atom', symbol: 'ATOM', decimals: 6, + denom: 'uatom', }, logoURI: '/logos/cosmos.svg', }; diff --git a/src/features/chains/metadata.ts b/src/features/chains/metadata.ts index aa05a5c7..21f242f9 100644 --- a/src/features/chains/metadata.ts +++ b/src/features/chains/metadata.ts @@ -57,7 +57,6 @@ export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList ], }, })); - // TODO cosmos cleanup here const assets = cosmosChains.map((c) => { if (!c.nativeToken) throw new Error(`Missing native token for ${c.name}`); return { @@ -66,19 +65,11 @@ export function getCosmosKitConfig(): { chains: CosmosChain[]; assets: AssetList { description: `The native token of ${c.displayName || c.name} chain.`, denom_units: [ - // { - // denom: `u${c.nativeToken.symbol}`, - // exponent: 0, - // }, { denom: 'token', exponent: c.nativeToken.decimals, }, ], - // base: `u${c.nativeToken.symbol}`, - // name: c.nativeToken.name, - // display: c.nativeToken.symbol, - // symbol: c.nativeToken.symbol, base: 'token', name: 'token', display: 'token', diff --git a/src/features/chains/utils.ts b/src/features/chains/utils.ts index 85d54404..bc9942b5 100644 --- a/src/features/chains/utils.ts +++ b/src/features/chains/utils.ts @@ -1,25 +1,22 @@ -import { chainIdToMetadata } from '@hyperlane-xyz/sdk'; +import { ChainNameOrId, 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: ChainNameOrId) { + return getMultiProvider().tryGetChainMetadata(chain); +} + +export function getChainMetadata(chain: ChainNameOrId) { + return getMultiProvider().getChainMetadata(chain); +} + +export function tryGetChainProtocol(chain: ChainNameOrId) { + return tryGetChainMetadata(chain)?.protocol; +} + +export function getChainProtocol(chain: ChainNameOrId) { + return getChainMetadata(chain).protocol; +} 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/store.ts b/src/features/store.ts index ab23a35f..62254634 100644 --- a/src/features/store.ts +++ b/src/features/store.ts @@ -1,10 +1,10 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import { FinalTransferStatuses, IgpQuote, TransferContext, TransferStatus } from './transfer/types'; +import { FinalTransferStatuses, TransferContext, TransferStatus } from './transfer/types'; // Increment this when persist state has breaking changes -const PERSIST_STATE_VERSION = 1; +const PERSIST_STATE_VERSION = 2; // Keeping everything here for now as state is simple // Will refactor into slices as necessary @@ -20,17 +20,6 @@ export interface AppState { failUnconfirmedTransfers: () => void; transferLoading: boolean; setTransferLoading: (isLoading: boolean) => void; - balances: { - senderTokenBalance: string; - senderNativeBalance: string; - senderNftIds: string[] | null; // null means unknown - isSenderNftOwner: boolean | null; - }; - setSenderBalances: (tokenBalance: string, nativeBalance: string) => void; - setSenderNftIds: (ids: string[] | null) => void; - setIsSenderNftOwner: (isOwner: boolean | null) => void; - igpQuote: IgpQuote | null; - setIgpQuote: (quote: IgpQuote | null) => void; } export const useStore = create()( @@ -66,27 +55,6 @@ export const useStore = create()( setTransferLoading: (isLoading) => { set(() => ({ transferLoading: isLoading })); }, - balances: { - senderTokenBalance: '0', - senderNativeBalance: '0', - senderNftIds: null, - isSenderNftOwner: false, - }, - setSenderBalances: (senderTokenBalance, senderNativeBalance) => { - set((state) => ({ - balances: { ...state.balances, senderTokenBalance, senderNativeBalance }, - })); - }, - setSenderNftIds: (senderNftIds) => { - set((state) => ({ balances: { ...state.balances, senderNftIds } })); - }, - setIsSenderNftOwner: (isSenderNftOwner) => { - set((state) => ({ balances: { ...state.balances, isSenderNftOwner } })); - }, - igpQuote: null, - setIgpQuote: (quote) => { - set(() => ({ igpQuote: quote })); - }, }), { name: 'app-state', 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..1a34c4fa 100644 --- a/src/features/tokens/SelectOrInputTokenIds.tsx +++ b/src/features/tokens/SelectOrInputTokenIds.tsx @@ -1,65 +1,35 @@ import { useFormikContext } from 'formik'; import { TextField } from '../../components/input/TextField'; -import { AssetNamespace, getCaip19Id } from '../caip/tokens'; -import { RouteType, RoutesMap } from '../routes/types'; -import { getTokenRoute, isWarpRoute } from '../routes/utils'; import { TransferFormValues } from '../transfer/types'; -import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol'; import { SelectTokenIdField } from './SelectTokenIdField'; -import { useContractSupportsTokenByOwner, useIsSenderNftOwner } from './balances'; -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: { tokenIndex }, } = 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; tokenIndex?: number }) { + // const { + // values: { amount }, + // } = useFormikContext(); + // useIsSenderNftOwner(token, amount); return (
diff --git a/src/features/tokens/SelectTokenIdField.tsx b/src/features/tokens/SelectTokenIdField.tsx index 4ba4402e..b00bb16b 100644 --- a/src/features/tokens/SelectTokenIdField.tsx +++ b/src/features/tokens/SelectTokenIdField.tsx @@ -6,15 +6,13 @@ 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; + tokenIndex?: number; 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 +20,8 @@ export function SelectTokenIdField({ name, tokenCaip19Id, disabled }: Props) { setTokenId(newTokenId); }; - 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..b970bc80 100644 --- a/src/features/tokens/TokenListModal.tsx +++ b/src/features/tokens/TokenListModal.tsx @@ -1,33 +1,28 @@ import Image from 'next/image'; import { useMemo, useState } from 'react'; +import { IToken } 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 { getWarpCore } from '../../context/context'; 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: IToken) => void; + origin: ChainName; + destination: ChainName; }) { const [search, setSearch] = useState(''); @@ -36,7 +31,7 @@ export function TokenListModal({ setSearch(''); }; - const onSelectAndClose = (token: TokenMetadata) => { + const onSelectAndClose = (token: IToken) => { onSelect(token); onClose(); }; @@ -57,9 +52,8 @@ export function TokenListModal({ autoComplete="off" /> @@ -68,82 +62,74 @@ 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: IToken) => void; }) { const tokens = useMemo(() => { const q = searchQuery?.trim().toLowerCase(); + const warpCore = getWarpCore(); + const multiChainTokens = warpCore.tokens.filter((t) => t.isMultiChainToken()); + const tokensWithRoute = warpCore.getTokensForRoute(origin, destination); return ( - getTokens() - .map((t) => { - const hasRoute = hasTokenRoute( - originCaip2Id, - destinationCaip2Id, - t.tokenCaip19Id, - tokenRoutes, - ); - return { ...t, disabled: !hasRoute }; - }) + multiChainTokens + .map((t) => ({ + token: t, + disabled: !tokensWithRoute.includes(t), + })) .sort((a, b) => { if (a.disabled && !b.disabled) return 1; else if (!a.disabled && b.disabled) return -1; else return 0; }) - // Remove duplicates - .filter((t, i, list) => i === list.findIndex((t2) => t2.tokenCaip19Id === t.tokenCaip19Id)) // Filter down to search query .filter((t) => { if (!q) return t; return ( - t.name.toLowerCase().includes(q) || - t.symbol.toLowerCase().includes(q) || - t.tokenCaip19Id.toLowerCase().includes(q) + t.token.name.toLowerCase().includes(q) || + t.token.symbol.toLowerCase().includes(q) || + t.token.addressOrDenom.toLowerCase().includes(q) ); }) // Hide/show disabled tokens .filter((t) => (config.showDisabledTokens ? true : !t.disabled)) ); - }, [searchQuery, originCaip2Id, destinationCaip2Id, tokenRoutes]); + }, [searchQuery, origin, destination]); return (
{tokens.length ? ( - tokens.map((t) => ( + tokens.map((t, i) => ( diff --git a/src/features/tokens/TokenSelectField.tsx b/src/features/tokens/TokenSelectField.tsx index babea6e9..db90d4cd 100644 --- a/src/features/tokens/TokenSelectField.tsx +++ b/src/features/tokens/TokenSelectField.tsx @@ -1,69 +1,56 @@ -import { useFormikContext } from 'formik'; +import { useField, useFormikContext } from 'formik'; import Image from 'next/image'; import { useEffect, useState } from 'react'; +import { IToken } from '@hyperlane-xyz/sdk'; + import { TokenIcon } from '../../components/icons/TokenIcon'; +import { getIndexForToken, getTokenByIndex, getWarpCore } from '../../context/context'; 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; disabled?: boolean; setIsNft: (value: boolean) => void; }; -export function TokenSelectField({ - name, - originCaip2Id, - destinationCaip2Id, - tokenRoutes, - disabled, - setIsNft, -}: Props) { - const { values, setFieldValue } = useFormikContext(); - // Keep local state for token details, but let formik manage field value - const [token, setToken] = useState(undefined); +export function TokenSelectField({ name, disabled, setIsNft }: Props) { + const { values } = useFormikContext(); + const [field, , helpers] = useField(name); const [isModalOpen, setIsModalOpen] = useState(false); const [isAutomaticSelection, setIsAutomaticSelection] = useState(false); - // Keep local state in sync with formik state + const { origin, destination } = values; useEffect(() => { - const routes = getTokenRoutes(originCaip2Id, destinationCaip2Id, tokenRoutes); - let newFieldValue: TokenCaip19Id | undefined = undefined; - let newToken: TokenMetadata | undefined = undefined; - let newIsAutomatic = true; - if (routes.length === 1) { - newFieldValue = routes[0].baseTokenCaip19Id; - newToken = getToken(newFieldValue); - } else if (routes.length > 1) { - newFieldValue = values[name] || routes[0].baseTokenCaip19Id; - newToken = getToken(newFieldValue!); + const tokensWithRoute = getWarpCore().getTokensForRoute(origin, destination); + let newFieldValue: number | undefined; + let newIsAutomatic: boolean; + // No tokens available for this route + if (tokensWithRoute.length === 0) { + newFieldValue = undefined; + newIsAutomatic = true; + } + // Exactly one found + else if (tokensWithRoute.length === 1) { + newFieldValue = getIndexForToken(tokensWithRoute[0]); + newIsAutomatic = true; + // Multiple possibilities + } else { + newFieldValue = undefined; newIsAutomatic = false; } - setToken(newToken); - setFieldValue(name, newFieldValue || ''); + helpers.setValue(newFieldValue); setIsAutomaticSelection(newIsAutomatic); - }, [name, token, values, originCaip2Id, destinationCaip2Id, tokenRoutes, setFieldValue]); + }, [origin, destination, helpers]); - const onSelectToken = (newToken: TokenMetadata) => { + const onSelectToken = (newToken: IToken) => { // Set the token address value in formik state - setFieldValue(name, newToken.tokenCaip19Id); - // reset amount after change token - setFieldValue('amount', ''); - // Update local state - setToken(newToken); + helpers.setValue(getIndexForToken(newToken)); // Update nft state in parent - setIsNft(!!isNonFungibleToken(newToken.tokenCaip19Id)); + setIsNft(newToken.isNft()); }; const onClickField = () => { @@ -73,8 +60,7 @@ export function TokenSelectField({ return ( <> setIsModalOpen(false)} onSelect={onSelectToken} - originCaip2Id={originCaip2Id} - destinationCaip2Id={destinationCaip2Id} - tokenRoutes={tokenRoutes} + origin={values.origin} + destination={values.destination} /> ); @@ -93,13 +78,11 @@ export function TokenSelectField({ function TokenButton({ token, - name, disabled, onClick, isAutomatic, }: { - token?: TokenMetadata; - name: string; + token?: IToken; disabled?: boolean; onClick?: () => void; isAutomatic?: boolean; @@ -107,7 +90,6 @@ function TokenButton({ return (
); } -function RecipientSection({ - tokenRoutes, - isReview, -}: { - tokenRoutes: RoutesMap; - isReview: boolean; -}) { +function RecipientSection({ isReview }: { isReview: boolean }) { const { values } = useFormikContext(); - const { balance, decimals } = useDestinationBalance(values, tokenRoutes); - - // 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: '', - recipientAddress: '', - }); - useEffect(() => { - if ( - recipientAddress && - balance && - prevRecipientBalance.current.balance && - prevRecipientBalance.current.recipientAddress === recipientAddress && - new BigNumber(balance).gt(prevRecipientBalance.current.balance) - ) { - toast.success('Recipient has received funds, transfer complete!'); - } - prevRecipientBalance.current = { balance, recipientAddress }; - }, [balance, recipientAddress, prevRecipientBalance]); + const { balance } = useDestinationBalance(values); + useRecipientBalanceWatcher(values.recipient, balance); return (
-
{`${label}: ${value}`}
; } function ButtonSection({ - tokenRoutes, isReview, + isValidating, setIsReview, }: { - tokenRoutes: RoutesMap; isReview: boolean; + isValidating: boolean; setIsReview: (b: boolean) => void; }) { const { values } = useFormikContext(); @@ -296,14 +229,14 @@ function ButtonSection({ const triggerTransactionsHandler = async () => { setTransferLoading(true); - await triggerTransactions(values, tokenRoutes); + await triggerTransactions(values); }; if (!isReview) { return ( ); @@ -326,25 +259,26 @@ 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; -}) { - const { setFieldValue } = useFormikContext(); - const onClick = () => { - if (balance && !disabled) setFieldValue('amount', fromWeiRounded(balance, decimals)); +function MaxButton({ balance, disabled }: { balance?: TokenAmount; disabled?: boolean }) { + const { values, setFieldValue } = useFormikContext(); + const { origin, destination, tokenIndex } = values; + const { accounts } = useAccounts(); + const { fetchMaxAmount, isLoading } = useFetchMaxAmount(); + + const onClick = async () => { + if (!balance || isNullish(tokenIndex) || disabled) return; + const maxAmount = await fetchMaxAmount({ balance, origin, destination, accounts }); + const decimalsAmount = maxAmount.getDecimalFormattedAmount(); + const roundedAmount = new BigNumber(decimalsAmount).toFixed(4, BigNumber.ROUND_FLOOR); + setFieldValue('amount', roundedAmount); }; + return ( - MAX + {isLoading ? ( +
+ +
+ ) : ( + 'MAX' + )}
); } function SelfButton({ disabled }: { disabled?: boolean }) { const { values, setFieldValue } = useFormikContext(); - const address = useAccountAddressForChain(values.destinationCaip2Id); + const address = useAccountAddressForChain(values.destination); const onClick = () => { if (disabled) return; - if (address) setFieldValue('recipientAddress', address); + if (address) setFieldValue('recipient', address); else toast.warn( `No account found for for chain ${getChainDisplayName( - values.destinationCaip2Id, + values.destination, )}, is your wallet connected?`, ); }; @@ -384,31 +324,23 @@ function SelfButton({ disabled }: { disabled?: boolean }) { ); } -function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes: RoutesMap }) { - const { - values: { amount, originCaip2Id, destinationCaip2Id, tokenCaip19Id }, - } = 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); +function ReviewDetails({ visible }: { visible: boolean }) { + const { values } = useFormikContext(); + const { amount, destination, tokenIndex } = values; + const originToken = getTokenByIndex(tokenIndex); const originTokenSymbol = originToken?.symbol || ''; + const connection = originToken?.getConnectionForChain(destination); + const destinationToken = connection?.token; + const isNft = originToken?.isNft(); + + const amountWei = isNft ? amount.toString() : toWei(amount, originToken?.decimals); const { isLoading: isApproveLoading, isApproveRequired } = useIsApproveRequired( - tokenCaip19Id, + originToken, amountWei, - route, visible, ); - const { isLoading: isQuoteLoading, igpQuote } = useIgpQuote(route); - const showIgpQuote = route && !isIbcOnlyRoute(route); + const { isLoading: isQuoteLoading, fees } = useFeeQuotes(values, visible); const isLoading = isApproveLoading || isQuoteLoading; @@ -419,73 +351,96 @@ function ReviewDetails({ visible, tokenRoutes }: { visible: boolean; tokenRoutes } overflow-hidden transition-all`} > - {isLoading ? ( -
- -
- ) : ( -
- {isApproveRequired && ( +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {isApproveRequired && ( +
+

Transaction 1: Approve Transfer

+
+

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

+ {originToken?.collateralAddressOrDenom && ( +

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

+ )} +
+
+ )}
-

Transaction 1: Approve Transfer

+

{`Transaction${isApproveRequired ? ' 2' : ''}: Transfer Remote`}

-

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

- {route?.baseRouterAddress && ( -

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

+ {destinationToken?.addressOrDenom && ( +

+ Remote Token + {destinationToken.addressOrDenom} +

)} -
-
- )} -
-

{`Transaction${isApproveRequired ? ' 2' : ''}: Transfer Remote`}

-
- {route?.destRouterAddress && ( -

- Remote Token - {route.destRouterAddress} -

- )} - {isNft ? (

- Token ID - {amount} + {isNft ? 'Token ID' : 'Amount'} + {`${amount} ${originTokenSymbol}`}

- ) : ( - <> + {fees?.localQuote && fees.localQuote.amount > 0n && (

- Amount - {`${amount} ${originTokenSymbol}`} + Local Gas (est.) + {`${fees.localQuote.getDecimalFormattedAmount().toFixed(4) || '0'} ${ + fees.localQuote.token.symbol || '' + }`}

- {showIgpQuote && ( -

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

- )} - - )} + )} + {fees?.interchainQuote && fees.interchainQuote.amount > 0n && ( +

+ Interchain Gas + {`${fees.interchainQuote.getDecimalFormattedAmount().toFixed(4) || '0'} ${ + fees.interchainQuote.token.symbol || '' + }`} +

+ )} +
-
-
- )} + + )} +
); } -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()[0]; + const connectedToken = firstToken.connections?.[0]; return { - originCaip2Id: firstRoute.originCaip2Id, - destinationCaip2Id: firstRoute.destCaip2Id, + origin: firstToken.chainName, + destination: connectedToken?.token?.chainName || '', + tokenIndex: getIndexForToken(firstToken), amount: '', - tokenCaip19Id: firstRoute.baseTokenCaip19Id, - recipientAddress: '', + recipient: '', }; - }, [chainCaip2Ids, tokenRoutes]); + }, []); +} + +async function validateForm( + values: TransferFormValues, + accounts: Record, +) { + try { + const { origin, destination, tokenIndex, amount, recipient } = values; + const token = getTokenByIndex(tokenIndex); + if (!token) return { token: 'Token is required' }; + const amountWei = toWei(amount, token.decimals); + const { address, publicKey: senderPubKey } = getAccountAddressAndPubKey(origin, accounts); + const result = await getWarpCore().validateTransfer({ + originTokenAmount: token.amount(amountWei), + destination, + recipient, + sender: address || '', + senderPubKey: await senderPubKey, + }); + return result; + } catch (error) { + logger.error('Error validating form', error); + return { form: errorToString(error) }; + } } diff --git a/src/features/transfer/TransfersDetailsModal.tsx b/src/features/transfer/TransfersDetailsModal.tsx index 629b65e0..724e0f6e 100644 --- a/src/features/transfer/TransfersDetailsModal.tsx +++ b/src/features/transfer/TransfersDetailsModal.tsx @@ -1,7 +1,6 @@ import Image from 'next/image'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { isZeroishAddress, toTitleCase } from '@hyperlane-xyz/utils'; import { MessageStatus, MessageTimeline, useMessageTimeline } from '@hyperlane-xyz/widgets'; import { Spinner } from '../../components/animation/Spinner'; @@ -10,6 +9,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, getWarpCore } from '../../context/context'; import LinkIcon from '../../images/icons/external-link-icon.svg'; import { formatTimestamp } from '../../utils/date'; import { getHypExplorerLink } from '../../utils/links'; @@ -21,11 +21,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'; @@ -43,33 +39,38 @@ export function TransfersDetailsModal({ const [toUrl, setToUrl] = useState(''); const [originTxUrl, setOriginTxUrl] = useState(''); - const { params, status, originTxHash, msgId, timestamp, activeAccountAddress } = transfer || {}; - const { destinationCaip2Id, originCaip2Id, tokenCaip19Id, amount, recipientAddress } = - params || {}; + const { + status, + origin, + destination, + amount, + sender, + recipient, + originTokenAddressOrDenom, + originTxHash, + msgId, + timestamp, + } = transfer || {}; - 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 { if (originTxHash) { - const originTxUrl = multiProvider.tryGetExplorerTxUrl(originChain, { hash: originTxHash }); + const originTxUrl = multiProvider.tryGetExplorerTxUrl(origin, { hash: originTxHash }); if (originTxUrl) setOriginTxUrl(fixDoubleSlash(originTxUrl)); } const [fromUrl, toUrl] = await Promise.all([ - multiProvider.tryGetExplorerAddressUrl(originChain, activeAccountAddress), - multiProvider.tryGetExplorerAddressUrl(destChain, recipientAddress), + multiProvider.tryGetExplorerAddressUrl(origin, sender), + multiProvider.tryGetExplorerAddressUrl(destination, recipient), ]); if (fromUrl) setFromUrl(fixDoubleSlash(fromUrl)); if (toUrl) setToUrl(fixDoubleSlash(toUrl)); } catch (error) { logger.error('Error fetching URLs:', error); } - }, [activeAccountAddress, originTxHash, multiProvider, recipientAddress, originChain, destChain]); + }, [sender, recipient, originTxHash, multiProvider, origin, destination]); useEffect(() => { if (!transfer) return; @@ -80,9 +81,9 @@ export function TransfersDetailsModal({ const isAccountReady = !!account?.isReady; const connectorName = account?.connectorName || 'wallet'; - const token = getToken(tokenCaip19Id); + const token = getWarpCore().findToken(origin, originTokenAddressOrDenom); - const isPermissionlessRoute = hasPermissionlessChain([destinationCaip2Id, originCaip2Id]); + const isPermissionlessRoute = hasPermissionlessChain([destination, origin]); const isSent = isTransferSent(status); const isFailed = isTransferFailed(status); @@ -100,7 +101,7 @@ export function TransfersDetailsModal({ [timestamp], ); - const explorerLink = getHypExplorerLink(originCaip2Id, msgId); + const explorerLink = getHypExplorerLink(origin, msgId); return (
{amount} - {token?.symbol || ''} - ({toTitleCase(tokenNamespace)}) + {token?.symbol}
- + - {getChainDisplayName(originCaip2Id, true)} + {getChainDisplayName(origin, true)}
@@ -152,18 +152,20 @@ export function TransfersDetailsModal({
- + - {getChainDisplayName(destinationCaip2Id, true)} + {getChainDisplayName(destination, true)}
{isFinal ? (
- - - {!isNative && } + + + {token?.addressOrDenom && ( + + )} {originTxHash && ( ; + balance: TokenAmount; + origin: ChainName; + destination: ChainName; +} + +export function useFetchMaxAmount() { + const mutation = useMutation({ + mutationFn: (params: FetchMaxParams) => fetchMaxAmount(params), + }); + return { fetchMaxAmount: mutation.mutateAsync, isLoading: mutation.isLoading }; +} + +async function fetchMaxAmount({ accounts, balance, destination, origin }: FetchMaxParams) { + try { + const { address, publicKey } = getAccountAddressAndPubKey(origin, accounts); + if (!address) return balance; + const maxAmount = await timeout( + getWarpCore().getMaxTransferAmount({ + balance, + destination, + sender: address, + senderPubKey: await publicKey, + }), + MAX_FETCH_TIMEOUT, + ); + return maxAmount; + } catch (error) { + logger.warn('Error or timeout fetching fee quotes for max amount', error); + return balance; + } +} diff --git a/src/features/transfer/types.ts b/src/features/transfer/types.ts index 38d6b866..8082107d 100644 --- a/src/features/transfer/types.ts +++ b/src/features/transfer/types.ts @@ -1,19 +1,16 @@ -import type { Route } from '../routes/types'; - export interface TransferFormValues { - originCaip2Id: ChainCaip2Id; - destinationCaip2Id: ChainCaip2Id; - tokenCaip19Id: TokenCaip19Id; + origin: ChainName; + destination: ChainName; + tokenIndex: number | undefined; amount: string; - recipientAddress: Address; + recipient: Address; } export enum TransferStatus { Preparing = 'preparing', - CreatingApprove = 'creating-approve', + CreatingTxs = 'creating-txs', SigningApprove = 'signing-approve', ConfirmingApprove = 'confirming-approve', - CreatingTransfer = 'creating-transfer', SigningTransfer = 'signing-transfer', ConfirmingTransfer = 'confirming-transfer', ConfirmedTransfer = 'confirmed-transfer', @@ -28,30 +25,14 @@ export const FinalTransferStatuses = [...SentTransferStatuses, TransferStatus.Fa export interface TransferContext { status: TransferStatus; - route: Route; - params: TransferFormValues; + origin: ChainName; + destination: ChainName; + originTokenAddressOrDenom?: string; + destTokenAddressOrDenom?: string; + amount: string; + sender: Address; + recipient: Address; originTxHash?: string; msgId?: string; 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/useBalanceWatcher.ts b/src/features/transfer/useBalanceWatcher.ts new file mode 100644 index 00000000..6e75308f --- /dev/null +++ b/src/features/transfer/useBalanceWatcher.ts @@ -0,0 +1,27 @@ +import { useEffect, useRef } from 'react'; +import { toast } from 'react-toastify'; + +import { TokenAmount } from '@hyperlane-xyz/sdk'; + +export function useRecipientBalanceWatcher(recipient?: Address, balance?: TokenAmount) { + // A crude way to detect transfer completions by triggering + // toast on recipient 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 prevRecipientBalance = useRef<{ balance?: TokenAmount; recipient?: string }>({ + recipient: '', + }); + useEffect(() => { + if ( + recipient && + balance && + prevRecipientBalance.current.balance && + prevRecipientBalance.current.recipient === recipient && + balance.token.equals(prevRecipientBalance.current.balance.token) && + balance.amount > prevRecipientBalance.current.balance.amount + ) { + toast.success('Recipient has received funds, transfer complete!'); + } + prevRecipientBalance.current = { balance, recipient: recipient }; + }, [balance, recipient, prevRecipientBalance]); +} diff --git a/src/features/transfer/useFeeQuotes.ts b/src/features/transfer/useFeeQuotes.ts new file mode 100644 index 00000000..399785a9 --- /dev/null +++ b/src/features/transfer/useFeeQuotes.ts @@ -0,0 +1,46 @@ +import { useQuery } from '@tanstack/react-query'; + +import { TokenAmount } from '@hyperlane-xyz/sdk'; +import { HexString } from '@hyperlane-xyz/utils'; + +import { getTokenByIndex, getWarpCore } from '../../context/context'; +import { logger } from '../../utils/logger'; +import { getAccountAddressAndPubKey, useAccounts } from '../wallet/hooks/multiProtocol'; + +import { TransferFormValues } from './types'; + +const FEE_QUOTE_REFRESH_INTERVAL = 15_000; // 10s + +export function useFeeQuotes( + { origin, destination, tokenIndex }: TransferFormValues, + enabled: boolean, +) { + const { accounts } = useAccounts(); + const { address: sender, publicKey: senderPubKey } = getAccountAddressAndPubKey(origin, accounts); + + const { isLoading, isError, data } = useQuery({ + queryKey: ['useFeeQuotes', destination, tokenIndex, sender], + queryFn: () => fetchFeeQuotes(destination, tokenIndex, sender, senderPubKey), + enabled, + refetchInterval: FEE_QUOTE_REFRESH_INTERVAL, + }); + + return { isLoading, isError, fees: data }; +} + +async function fetchFeeQuotes( + destination?: ChainName, + tokenIndex?: number, + sender?: Address, + senderPubKey?: Promise, +): Promise<{ interchainQuote: TokenAmount; localQuote: TokenAmount } | null> { + const originToken = getTokenByIndex(tokenIndex); + if (!destination || !sender || !originToken) return null; + logger.debug('Fetching fee quotes'); + return getWarpCore().estimateTransferRemoteFees({ + originToken, + destination, + sender, + senderPubKey: await senderPubKey, + }); +} diff --git a/src/features/transfer/useIgpQuote.ts b/src/features/transfer/useIgpQuote.ts deleted file mode 100644 index feebe80e..00000000 --- a/src/features/transfer/useIgpQuote.ts +++ /dev/null @@ -1,118 +0,0 @@ -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 { 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); - - const { isLoading, isError, error, data } = useQuery({ - queryKey: ['useIgpQuote', route], - queryFn: () => { - if (!route || isIbcOnlyRoute(route)) return null; - return fetchIgpQuote(route); - }, - }); - - 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..71c4d357 100644 --- a/src/features/transfer/useTokenTransfer.ts +++ b/src/features/transfer/useTokenTransfer.ts @@ -1,49 +1,21 @@ -import { MsgTransferEncodeObject } from '@cosmjs/stargate'; -import type { Transaction as SolTransaction } from '@solana/web3.js'; -import { - SendTransactionArgs as ViemTransactionRequest, - WaitForTransactionResult as ViemViemTransactionReceipt, -} from '@wagmi/core'; -import BigNumber from 'bignumber.js'; -import { PopulatedTransaction as Ethers5Transaction } from 'ethers'; import { useCallback, useState } from 'react'; import { toast } from 'react-toastify'; -import { - CosmIbcToWarpTokenAdapter, - CosmIbcTokenAdapter, - IHypTokenAdapter, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType, toWei } from '@hyperlane-xyz/utils'; +import { WarpTxCategory } from '@hyperlane-xyz/sdk'; +import { toTitleCase, toWei } from '@hyperlane-xyz/utils'; import { toastTxSuccess } from '../../components/toast/TxSuccessToast'; +import { getTokenByIndex, getWarpCore } from '../../context/context'; import { logger } from '../../utils/logger'; -import { parseCaip2Id } from '../caip/chains'; -import { isNonFungibleToken } from '../caip/tokens'; -import { getChainMetadata, getMultiProvider } from '../multiProvider'; -import { Route, RoutesMap } from '../routes/types'; -import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isWarpRoute } from '../routes/utils'; import { AppState, useStore } from '../store'; -import { AdapterFactory } from '../tokens/AdapterFactory'; -import { isApproveRequired } from '../tokens/approval'; import { getAccountAddressForChain, useAccounts, useActiveChains, useTransactionFns, } from '../wallet/hooks/multiProtocol'; -import { ActiveChainInfo, SendTransactionFn } from '../wallet/hooks/types'; -import { ethers5TxToWagmiTx } from '../wallet/utils'; -import { - IgpQuote, - IgpTokenType, - TransferContext, - TransferFormValues, - TransferStatus, -} from './types'; -import { fetchIgpQuote } from './useIgpQuote'; -import { ensureSufficientCollateral, tryGetMsgIdFromEvmTransferReceipt } from './utils'; +import { TransferContext, TransferFormValues, TransferStatus } from './types'; export function useTokenTransfer(onDone?: () => void) { const { transfers, addTransfer, updateTransferStatus } = useStore((s) => ({ @@ -61,10 +33,9 @@ export function useTokenTransfer(onDone?: () => void) { // TODO implement cancel callback for when modal is closed? const triggerTransactions = useCallback( - (values: TransferFormValues, tokenRoutes: RoutesMap) => + (values: TransferFormValues) => executeTransfer({ values, - tokenRoutes, transferIndex, activeAccounts, activeChains, @@ -94,7 +65,6 @@ export function useTokenTransfer(onDone?: () => void) { async function executeTransfer({ values, - tokenRoutes, transferIndex, activeAccounts, activeChains, @@ -105,7 +75,6 @@ async function executeTransfer({ onDone, }: { values: TransferFormValues; - tokenRoutes: RoutesMap; transferIndex: number; activeAccounts: ReturnType; activeChains: ReturnType; @@ -117,74 +86,89 @@ async function executeTransfer({ }) { logger.debug('Preparing transfer transaction(s)'); setIsLoading(true); - let status: TransferStatus = TransferStatus.Preparing; + let transferStatus: TransferStatus = TransferStatus.Preparing; + updateTransferStatus(transferIndex, transferStatus); try { - const { originCaip2Id, destinationCaip2Id, tokenCaip19Id, amount, recipientAddress } = values; - const { protocol: originProtocol } = parseCaip2Id(originCaip2Id); - const { reference: destReference } = parseCaip2Id(destinationCaip2Id); - const destinationDomainId = getMultiProvider().getDomainId(destReference); + const { origin, destination, tokenIndex, amount, recipient } = values; + const originToken = getTokenByIndex(tokenIndex); + const connection = originToken?.getConnectionForChain(destination); + if (!originToken || !connection) throw new Error('No token route found between chains'); - const tokenRoute = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); - if (!tokenRoute) throw new Error('No token route found between chains'); + const originProtocol = originToken.protocol; + const isNft = originToken.isNft(); + const weiAmountOrId = isNft ? amount : toWei(amount, originToken.decimals); + const originTokenAmount = originToken.amount(weiAmountOrId); - const isNft = isNonFungibleToken(tokenCaip19Id); - const weiAmountOrId = isNft ? amount : toWei(amount, tokenRoute.originDecimals); - const activeAccountAddress = getAccountAddressForChain( - originCaip2Id, - activeAccounts.accounts[originProtocol], - ); - if (!activeAccountAddress) throw new Error('No active account found for origin chain'); + const sendTransaction = transactionFns[originProtocol].sendTransaction; const activeChain = activeChains.chains[originProtocol]; + const sender = getAccountAddressForChain(origin, activeAccounts.accounts); + if (!sender) throw new Error('No active account found for origin chain'); + + const warpCore = getWarpCore(); + + const isCollateralSufficient = await warpCore.isDestinationCollateralSufficient({ + originTokenAmount, + destination, + }); + if (!isCollateralSufficient) { + toast.error('Insufficient collateral on destination for transfer'); + throw new Error('Insufficient destination collateral'); + } addTransfer({ - activeAccountAddress, timestamp: new Date().getTime(), - status, - route: tokenRoute, - params: values, + status: TransferStatus.Preparing, + origin, + destination, + originTokenAddressOrDenom: originToken.addressOrDenom, + destTokenAddressOrDenom: connection.token.addressOrDenom, + sender, + recipient, + amount, }); - const executeParams: ExecuteTransferParams = { - weiAmountOrId, - originProtocol, - destinationDomainId, - recipientAddress, - tokenRoute, - activeAccountAddress, - activeChain, - updateStatus: (s: TransferStatus) => { - status = s; - updateTransferStatus(transferIndex, s); - }, - sendTransaction: transactionFns[originProtocol].sendTransaction, - }; + updateTransferStatus(transferIndex, (transferStatus = TransferStatus.CreatingTxs)); - let transferTxHash: string; - let msgId: string | undefined; - if (isWarpRoute(tokenRoute)) { - ({ transferTxHash, msgId } = await executeHypTransfer(executeParams)); - } else if (isIbcRoute(tokenRoute)) { - ({ transferTxHash } = await executeIbcTransfer(executeParams)); - } else { - throw new Error('Unsupported route type'); + const txs = await warpCore.getTransferRemoteTxs({ + originTokenAmount, + destination, + sender, + recipient, + }); + + const hashes: string[] = []; + for (const tx of txs) { + updateTransferStatus(transferIndex, (transferStatus = txCategoryToStatuses[tx.category][0])); + const { hash, confirm } = await sendTransaction({ + tx: tx.transaction, + chainName: origin, + activeChainName: activeChain.chainName, + providerType: tx.type, + }); + updateTransferStatus(transferIndex, (transferStatus = txCategoryToStatuses[tx.category][1])); + const receipt = await confirm(); + const description = toTitleCase(tx.category); + logger.debug(`${description} transaction confirmed, hash:`, receipt.transactionHash); + toastTxSuccess(`${description} transaction sent!`, receipt.transactionHash, origin); + hashes.push(hash); } - updateTransferStatus(transferIndex, (status = TransferStatus.ConfirmedTransfer), { - originTxHash: transferTxHash, - msgId, - }); + // TODO + // const msgId = tryGetMsgIdFromTransferReceipt(transferReceipt); - logger.debug('Transfer transaction confirmed, hash:', transferTxHash); - toastTxSuccess('Remote transfer started!', transferTxHash, originCaip2Id); + updateTransferStatus(transferIndex, (transferStatus = TransferStatus.ConfirmedTransfer), { + originTxHash: hashes.at(-1), + msgId: '', + }); } catch (error) { - logger.error(`Error at stage ${status}`, error); + logger.error(`Error at stage ${transferStatus}`, error); updateTransferStatus(transferIndex, TransferStatus.Failed); if (JSON.stringify(error).includes('ChainMismatchError')) { // Wagmi switchNetwork call helps prevent this but isn't foolproof toast.error('Wallet must be connected to origin chain'); } else { - toast.error(errorMessages[status] || 'Unable to transfer tokens.'); + toast.error(errorMessages[transferStatus] || 'Unable to transfer tokens.'); } } @@ -192,261 +176,16 @@ async function executeTransfer({ if (onDone) onDone(); } -interface ExecuteTransferParams { - weiAmountOrId: string; - originProtocol: ProtocolType; - destinationDomainId: DomainId; - recipientAddress: Address; - tokenRoute: Route; - activeAccountAddress: Address; - activeChain: ActiveChainInfo; - updateStatus: (s: TransferStatus) => void; - sendTransaction: SendTransactionFn; -} - -interface ExecuteHypTransferParams extends ExecuteTransferParams { - hypTokenAdapter: IHypTokenAdapter; - igpQuote: IgpQuote; -} - -async function executeHypTransfer(params: ExecuteTransferParams) { - const { tokenRoute, weiAmountOrId, originProtocol } = params; - const hypTokenAdapter = AdapterFactory.HypTokenAdapterFromRouteOrigin(tokenRoute); - - await ensureSufficientCollateral(tokenRoute, weiAmountOrId); - - const igpQuote = await fetchIgpQuote(tokenRoute, hypTokenAdapter); - const hypTransferParams: ExecuteHypTransferParams = { - ...params, - hypTokenAdapter, - igpQuote, - }; - - let result: { transferTxHash: string; msgId?: string }; - if (originProtocol === ProtocolType.Ethereum) { - result = await executeEvmTransfer(hypTransferParams); - } else if (originProtocol === ProtocolType.Sealevel) { - result = await executeSealevelTransfer(hypTransferParams); - } else if (originProtocol === ProtocolType.Cosmos) { - result = await executeCosmWasmTransfer(hypTransferParams); - } else { - throw new Error(`Unsupported protocol type: ${originProtocol}`); - } - return result; -} - -async function executeEvmTransfer({ - weiAmountOrId, - destinationDomainId, - recipientAddress, - tokenRoute, - hypTokenAdapter, - igpQuote, - activeAccountAddress, - activeChain, - updateStatus, - sendTransaction, -}: ExecuteHypTransferParams) { - if (!isWarpRoute(tokenRoute)) throw new Error('Unsupported route type'); - const { baseRouterAddress, originCaip2Id, baseTokenCaip19Id } = tokenRoute; - - const isApproveTxRequired = - activeAccountAddress && - (await isApproveRequired(tokenRoute, baseTokenCaip19Id, weiAmountOrId, activeAccountAddress)); - - if (isApproveTxRequired) { - updateStatus(TransferStatus.CreatingApprove); - const tokenAdapter = AdapterFactory.TokenAdapterFromAddress(baseTokenCaip19Id); - const approveTxRequest = (await tokenAdapter.populateApproveTx({ - weiAmountOrId, - recipient: baseRouterAddress, - })) as Ethers5Transaction; - - updateStatus(TransferStatus.SigningApprove); - const { confirm: confirmApprove } = await sendTransaction({ - tx: ethers5TxToWagmiTx(approveTxRequest), - chainCaip2Id: originCaip2Id, - activeCap2Id: activeChain.chainCaip2Id, - }); - - updateStatus(TransferStatus.ConfirmingApprove); - const approveTxReceipt = await confirmApprove(); - logger.debug('Approve transaction confirmed, hash:', approveTxReceipt.transactionHash); - toastTxSuccess('Approve transaction sent!', approveTxReceipt.transactionHash, originCaip2Id); - } - - updateStatus(TransferStatus.CreatingTransfer); - - logger.debug('Quoted gas payment', igpQuote.weiAmount); - // If sending native tokens (e.g. Eth), the gasPayment must be added to the tx value and sent together - const txValue = - igpQuote.type === IgpTokenType.NativeCombined - ? BigNumber(igpQuote.weiAmount).plus(weiAmountOrId).toFixed(0) - : igpQuote.weiAmount; - const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ - weiAmountOrId: weiAmountOrId.toString(), - recipient: recipientAddress, - destination: destinationDomainId, - txValue, - })) as Ethers5Transaction; - - updateStatus(TransferStatus.SigningTransfer); - const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({ - tx: ethers5TxToWagmiTx(transferTxRequest), - chainCaip2Id: originCaip2Id, - activeCap2Id: activeChain.chainCaip2Id, - }); - - updateStatus(TransferStatus.ConfirmingTransfer); - const transferReceipt = await confirmTransfer(); - const msgId = tryGetMsgIdFromEvmTransferReceipt(transferReceipt); - - return { transferTxHash, msgId }; -} - -async function executeSealevelTransfer({ - weiAmountOrId, - destinationDomainId, - recipientAddress, - tokenRoute, - hypTokenAdapter, - activeAccountAddress, - activeChain, - updateStatus, - sendTransaction, -}: ExecuteHypTransferParams) { - const { originCaip2Id } = tokenRoute; - - updateStatus(TransferStatus.CreatingTransfer); - - // TODO solana enable gas payments? - // logger.debug('Quoted gas payment', igpQuote.weiAmount); - - const transferTxRequest = (await hypTokenAdapter.populateTransferRemoteTx({ - weiAmountOrId, - destination: destinationDomainId, - recipient: recipientAddress, - fromAccountOwner: activeAccountAddress, - })) as SolTransaction; - - updateStatus(TransferStatus.SigningTransfer); - - const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({ - tx: transferTxRequest, - chainCaip2Id: originCaip2Id, - activeCap2Id: activeChain.chainCaip2Id, - }); - - updateStatus(TransferStatus.ConfirmingTransfer); - await confirmTransfer(); - - return { transferTxHash }; -} - -async function executeCosmWasmTransfer({ - weiAmountOrId, - destinationDomainId, - recipientAddress, - tokenRoute, - hypTokenAdapter, - igpQuote, - activeChain, - updateStatus, - sendTransaction, -}: ExecuteHypTransferParams) { - updateStatus(TransferStatus.CreatingTransfer); - - const transferTxRequest = await hypTokenAdapter.populateTransferRemoteTx({ - weiAmountOrId, - recipient: recipientAddress, - destination: destinationDomainId, - txValue: igpQuote.weiAmount, - }); - - updateStatus(TransferStatus.SigningTransfer); - const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({ - tx: { type: 'cosmwasm', request: transferTxRequest }, - chainCaip2Id: tokenRoute.originCaip2Id, - activeCap2Id: activeChain.chainCaip2Id, - }); - - updateStatus(TransferStatus.ConfirmingTransfer); - await confirmTransfer(); - - return { transferTxHash }; -} - -async function executeIbcTransfer({ - weiAmountOrId, - destinationDomainId, - recipientAddress, - tokenRoute, - activeChain, - activeAccountAddress, - updateStatus, - sendTransaction, -}: ExecuteTransferParams) { - if (!isIbcRoute(tokenRoute)) throw new Error('Unsupported route type'); - updateStatus(TransferStatus.CreatingTransfer); - - const multiProvider = getMultiProvider(); - const chainName = getChainMetadata(tokenRoute.originCaip2Id).name; - const adapterProperties = { - ibcDenom: tokenRoute.originIbcDenom, - sourcePort: tokenRoute.sourcePort, - sourceChannel: tokenRoute.sourceChannel, - }; - - let adapter: IHypTokenAdapter; - let txValue: string | undefined = undefined; - if (isIbcOnlyRoute(tokenRoute)) { - adapter = new CosmIbcTokenAdapter(chainName, multiProvider, {}, adapterProperties); - } else { - const intermediateChainName = getChainMetadata(tokenRoute.intermediateCaip2Id).name; - adapter = new CosmIbcToWarpTokenAdapter( - chainName, - multiProvider, - { - intermediateRouterAddress: tokenRoute.intermediateRouterAddress, - destinationRouterAddress: tokenRoute.destRouterAddress, - }, - { - ...adapterProperties, - derivedIbcDenom: tokenRoute.derivedIbcDenom, - intermediateChainName, - }, - ); - const igpQuote = await fetchIgpQuote(tokenRoute, adapter); - txValue = igpQuote.weiAmount; - } - - const transferTxRequest = (await adapter.populateTransferRemoteTx({ - weiAmountOrId, - recipient: recipientAddress, - fromAccountOwner: activeAccountAddress, - destination: destinationDomainId, - txValue, - })) as MsgTransferEncodeObject; - - updateStatus(TransferStatus.SigningTransfer); - const { hash: transferTxHash, confirm: confirmTransfer } = await sendTransaction({ - tx: { type: 'stargate', request: transferTxRequest }, - chainCaip2Id: tokenRoute.originCaip2Id, - activeCap2Id: activeChain.chainCaip2Id, - }); - - updateStatus(TransferStatus.ConfirmingTransfer); - await confirmTransfer(); - - return { transferTxHash }; -} - const errorMessages: Partial> = { [TransferStatus.Preparing]: 'Error while preparing the transactions.', - [TransferStatus.CreatingApprove]: 'Error while creating the approve transaction.', + [TransferStatus.CreatingTxs]: 'Error while creating the transactions.', [TransferStatus.SigningApprove]: 'Error while signing the approve transaction.', [TransferStatus.ConfirmingApprove]: 'Error while confirming the approve transaction.', - [TransferStatus.CreatingTransfer]: 'Error while creating the transfer transaction.', [TransferStatus.SigningTransfer]: 'Error while signing the transfer transaction.', [TransferStatus.ConfirmingTransfer]: 'Error while confirming the transfer transaction.', }; + +const txCategoryToStatuses: Record = { + [WarpTxCategory.Approval]: [TransferStatus.SigningApprove, TransferStatus.ConfirmingApprove], + [WarpTxCategory.Transfer]: [TransferStatus.SigningTransfer, TransferStatus.ConfirmingTransfer], +}; diff --git a/src/features/transfer/utils.ts b/src/features/transfer/utils.ts index 5772517f..84818d10 100644 --- a/src/features/transfer/utils.ts +++ b/src/features/transfer/utils.ts @@ -1,46 +1,11 @@ -import BigNumber from 'bignumber.js'; -import { toast } from 'react-toastify'; import { TransactionReceipt } from 'viem'; import { HyperlaneCore } from '@hyperlane-xyz/sdk'; -import { ProtocolType, convertDecimals } from '@hyperlane-xyz/utils'; import { logger } from '../../utils/logger'; -import { getProtocolType } from '../caip/chains'; -import { isNonFungibleToken } from '../caip/tokens'; -import { Route } from '../routes/types'; -import { isRouteToCollateral, isWarpRoute } from '../routes/utils'; -import { AdapterFactory } from '../tokens/AdapterFactory'; -// In certain cases, like when a synthetic token has >1 collateral tokens -// it's possible that the collateral contract balance is insufficient to -// cover the remote transfer. This ensures the balance is sufficient or throws. -export async function ensureSufficientCollateral(route: Route, weiAmount: string) { - if (!isRouteToCollateral(route) || isNonFungibleToken(route.baseTokenCaip19Id)) return; - - // TODO cosmos support here - if ( - getProtocolType(route.originCaip2Id) === ProtocolType.Cosmos || - getProtocolType(route.destCaip2Id) === ProtocolType.Cosmos || - !isWarpRoute(route) - ) - return; - - logger.debug('Ensuring collateral balance for route', route); - const adapter = AdapterFactory.HypTokenAdapterFromRouteDest(route); - const destinationBalance = await adapter.getBalance(route.destRouterAddress); - const destinationBalanceInOriginDecimals = convertDecimals( - route.destDecimals, - route.originDecimals, - destinationBalance, - ); - if (new BigNumber(destinationBalanceInOriginDecimals).lt(weiAmount)) { - toast.error('Collateral contract balance insufficient for transfer'); - throw new Error('Insufficient collateral balance'); - } -} - -export function tryGetMsgIdFromEvmTransferReceipt(receipt: TransactionReceipt) { +// TODO multiprotocol +export function tryGetMsgIdFromTransferReceipt(receipt: TransactionReceipt) { try { // TODO viem // @ts-ignore 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..1e0e5a0c 100644 --- a/src/features/wallet/SideBarMenu.tsx +++ b/src/features/wallet/SideBarMenu.tsx @@ -2,12 +2,11 @@ import Image from 'next/image'; import { useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'react-toastify'; -import { toTitleCase } from '@hyperlane-xyz/utils'; - import { SmallSpinner } from '../../components/animation/SmallSpinner'; import { ChainLogo } from '../../components/icons/ChainLogo'; import { Identicon } from '../../components/icons/Identicon'; import { PLACEHOLDER_COSMOS_CHAIN } from '../../consts/values'; +import { getWarpCore } from '../../context/context'; import ArrowRightIcon from '../../images/icons/arrow-right.svg'; import CollapseIcon from '../../images/icons/collapse-icon.svg'; import Logout from '../../images/icons/logout.svg'; @@ -15,14 +14,13 @@ import ResetIcon from '../../images/icons/reset-icon.svg'; import Wallet from '../../images/icons/wallet.svg'; import { tryClipboardSet } from '../../utils/clipboard'; import { STATUSES_WITH_ICON, getIconByTransferStatus } from '../../utils/transfer'; -import { getAssetNamespace } from '../caip/tokens'; import { getChainDisplayName } from '../chains/utils'; import { useStore } from '../store'; -import { getToken } from '../tokens/metadata'; import { TransfersDetailsModal } from '../transfer/TransfersDetailsModal'; import { TransferContext } from '../transfer/types'; import { useAccounts, useDisconnectFns } from './hooks/multiProtocol'; +import { AccountInfo } from './hooks/types'; export function SideBarMenu({ onConnectWallet, @@ -59,12 +57,6 @@ export function SideBarMenu({ setIsMenuOpen(isOpen); }, [isOpen]); - const onClickCopy = (value?: string) => async () => { - if (!value) return; - await tryClipboardSet(value); - toast.success('Address copied to clipboard', { autoClose: 2000 }); - }; - const onClickDisconnect = async () => { for (const disconnectFn of Object.values(disconnects)) { await disconnectFn(); @@ -96,28 +88,10 @@ export function SideBarMenu({ Connected Wallets
- {readyAccounts.map((acc) => - acc.addresses.map((addr) => { - if (addr?.chainCaip2Id?.includes(PLACEHOLDER_COSMOS_CHAIN)) return null; - return ( - - ); + {readyAccounts.map((acc, i) => + acc.addresses.map((addr, j) => { + if (addr?.chainName?.includes(PLACEHOLDER_COSMOS_CHAIN)) return null; + return ; }), )} + /> ))}
{sortedTransfers?.length > 0 && ( @@ -217,6 +143,79 @@ export function SideBarMenu({ ); } +function AccountSummary({ account, address }: { account: AccountInfo; address: Address }) { + const onClickCopy = async () => { + if (!address) return; + await tryClipboardSet(address); + toast.success('Address copied to clipboard', { autoClose: 2000 }); + }; + + return ( + + ); +} + +function TransferSummary({ + transfer, + onClick, +}: { + transfer: TransferContext; + onClick: () => void; +}) { + const { amount, origin, destination, status, timestamp, originTokenAddressOrDenom } = transfer; + const token = getWarpCore().findToken(origin, originTokenAddressOrDenom); + + return ( + + ); +} + function Icon({ src, alt, 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/cosmos.ts b/src/features/wallet/hooks/cosmos.ts index 860b9371..14d7de48 100644 --- a/src/features/wallet/hooks/cosmos.ts +++ b/src/features/wallet/hooks/cosmos.ts @@ -3,35 +3,36 @@ import { useChain, useChains } from '@cosmos-kit/react'; import { useCallback, useMemo } from 'react'; import { toast } from 'react-toastify'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProviderType } from '@hyperlane-xyz/sdk'; +import { HexString, ProtocolType } from '@hyperlane-xyz/utils'; import { PLACEHOLDER_COSMOS_CHAIN } from '../../../consts/values'; import { logger } from '../../../utils/logger'; -import { getCaip2Id } from '../../caip/chains'; import { getCosmosChainNames } from '../../chains/metadata'; -import { getChainMetadata, getMultiProvider } from '../../multiProvider'; +import { getChainMetadata } from '../../chains/utils'; import { AccountInfo, ActiveChainInfo, ChainAddress, ChainTransactionFns } from './types'; export function useCosmosAccount(): AccountInfo { const chainToContext = useChains(getCosmosChainNames()); return useMemo(() => { - const cosmAddresses: Array = []; - let cosmConnectorName: string | undefined = undefined; - let isCosmAccountReady = false; - const multiProvider = getMultiProvider(); + const addresses: Array = []; + let publicKey: Promise | undefined = undefined; + let connectorName: string | undefined = undefined; + let isReady = false; for (const [chainName, context] of Object.entries(chainToContext)) { if (!context.address) continue; - const caip2Id = getCaip2Id(ProtocolType.Cosmos, multiProvider.getChainId(chainName)); - cosmAddresses.push({ address: context.address, chainCaip2Id: caip2Id }); - isCosmAccountReady = true; - cosmConnectorName ||= context.wallet?.prettyName; + addresses.push({ address: context.address, chainName }); + publicKey = context.getAccount().then((acc) => Buffer.from(acc.pubkey).toString('hex')); + isReady = true; + connectorName ||= context.wallet?.prettyName; } return { protocol: ProtocolType.Cosmos, - addresses: cosmAddresses, - connectorName: cosmConnectorName, - isReady: isCosmAccountReady, + addresses, + publicKey, + connectorName, + isReady, }; }, [chainToContext]); } @@ -56,36 +57,37 @@ export function useCosmosActiveChain(): ActiveChainInfo { export function useCosmosTransactionFns(): ChainTransactionFns { const chainToContext = useChains(getCosmosChainNames()); - const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => { - const chainName = getChainMetadata(chainCaip2Id).displayName; - toast.warn(`Cosmos wallet must be connected to origin chain ${chainName}}`); + const onSwitchNetwork = useCallback(async (chainName: ChainName) => { + const displayName = getChainMetadata(chainName).displayName || chainName; + toast.warn(`Cosmos wallet must be connected to origin chain ${displayName}}`); }, []); const onSendTx = useCallback( async ({ tx, - chainCaip2Id, - activeCap2Id, + chainName, + activeChainName, + providerType, }: { - tx: { type: 'cosmwasm' | 'stargate'; request: any }; - chainCaip2Id: ChainCaip2Id; - activeCap2Id?: ChainCaip2Id; + tx: any; + chainName: ChainName; + activeChainName?: ChainName; + providerType?: ProviderType; }) => { - const chainName = getChainMetadata(chainCaip2Id).name; const chainContext = chainToContext[chainName]; if (!chainContext?.address) throw new Error(`Cosmos wallet not connected for ${chainName}`); - if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id); - logger.debug(`Sending ${tx.type} tx on chain ${chainCaip2Id}`); + if (activeChainName && activeChainName !== chainName) await onSwitchNetwork(chainName); + logger.debug(`Sending tx on chain ${chainName}`); const { getSigningCosmWasmClient, getSigningStargateClient } = chainContext; let result: ExecuteResult | DeliverTxResponse; - if (tx.type === 'cosmwasm') { + if (providerType === ProviderType.CosmJsWasm) { const client = await getSigningCosmWasmClient(); - result = await client.executeMultiple(chainContext.address, [tx.request], 'auto'); - } else if (tx.type === 'stargate') { + result = await client.executeMultiple(chainContext.address, [tx], 'auto'); + } else if (providerType === ProviderType.CosmJs) { const client = await getSigningStargateClient(); - result = await client.signAndBroadcast(chainContext.address, [tx.request], 'auto'); + result = await client.signAndBroadcast(chainContext.address, [tx], 'auto'); } else { - throw new Error('Invalid cosmos tx type'); + throw new Error(`Invalid cosmos provider type ${providerType}`); } const confirm = async () => { diff --git a/src/features/wallet/hooks/evm.ts b/src/features/wallet/hooks/evm.ts index 5e6cee75..80830ad7 100644 --- a/src/features/wallet/hooks/evm.ts +++ b/src/features/wallet/hooks/evm.ts @@ -1,17 +1,13 @@ import { useConnectModal } from '@rainbow-me/rainbowkit'; -import { - SendTransactionArgs, - sendTransaction, - switchNetwork, - waitForTransaction, -} from '@wagmi/core'; +import { sendTransaction, switchNetwork, waitForTransaction } from '@wagmi/core'; import { useCallback, useMemo } from 'react'; import { useAccount, useDisconnect, useNetwork } from 'wagmi'; import { ProtocolType, sleep } from '@hyperlane-xyz/utils'; import { logger } from '../../../utils/logger'; -import { getCaip2Id, getEthereumChainId } from '../../caip/chains'; +import { getChainMetadata, tryGetChainMetadata } from '../../chains/utils'; +import { ethers5TxToWagmiTx } from '../utils'; import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types'; @@ -46,15 +42,15 @@ export function useEvmActiveChain(): ActiveChainInfo { return useMemo( () => ({ chainDisplayName: chain?.name, - chainCaip2Id: chain ? getCaip2Id(ProtocolType.Ethereum, chain.id) : undefined, + chainName: chain ? tryGetChainMetadata(chain.id)?.name : undefined, }), [chain], ); } export function useEvmTransactionFns(): ChainTransactionFns { - const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => { - const chainId = getEthereumChainId(chainCaip2Id); + const onSwitchNetwork = useCallback(async (chainName: ChainName) => { + const chainId = getChainMetadata(chainName).chainId as number; await switchNetwork({ chainId }); // Some wallets seem to require a brief pause after switch await sleep(2000); @@ -67,19 +63,20 @@ export function useEvmTransactionFns(): ChainTransactionFns { const onSendTx = useCallback( async ({ tx, - chainCaip2Id, - activeCap2Id, + chainName, + activeChainName, }: { - tx: SendTransactionArgs; - chainCaip2Id: ChainCaip2Id; - activeCap2Id?: ChainCaip2Id; + tx: any; + chainName: ChainName; + activeChainName?: ChainName; }) => { - if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id); - const chainId = getEthereumChainId(chainCaip2Id); - logger.debug(`Sending tx on chain ${chainCaip2Id}`); + if (activeChainName && activeChainName !== chainName) await onSwitchNetwork(chainName); + logger.debug(`Sending tx on chain ${chainName}`); + const chainId = getChainMetadata(chainName).chainId as number; + const wagmiTx = ethers5TxToWagmiTx(tx); const { hash } = await sendTransaction({ chainId, - ...tx, + ...wagmiTx, }); const confirm = () => waitForTransaction({ chainId, hash, confirmations: 1 }); return { hash, confirm }; diff --git a/src/features/wallet/hooks/multiProtocol.tsx b/src/features/wallet/hooks/multiProtocol.tsx index 829ecfc9..d4bfcced 100644 --- a/src/features/wallet/hooks/multiProtocol.tsx +++ b/src/features/wallet/hooks/multiProtocol.tsx @@ -1,11 +1,11 @@ import { useMemo } from 'react'; import { toast } from 'react-toastify'; -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { HexString, ProtocolType } from '@hyperlane-xyz/utils'; import { config } from '../../../consts/config'; import { logger } from '../../../utils/logger'; -import { tryGetProtocolType } from '../../caip/chains'; +import { getChainProtocol, tryGetChainProtocol } from '../../chains/utils'; import { useCosmosAccount, @@ -67,31 +67,44 @@ export function useAccounts(): { ); } -export function useAccountForChain(chainCaip2Id?: ChainCaip2Id): AccountInfo | undefined { +export function useAccountForChain(chainName?: ChainName): AccountInfo | undefined { const { accounts } = useAccounts(); - if (!chainCaip2Id) return undefined; - const protocol = tryGetProtocolType(chainCaip2Id); + if (!chainName) return undefined; + const protocol = tryGetChainProtocol(chainName); if (!protocol) return undefined; - return accounts[protocol]; + return accounts?.[protocol]; } -export function useAccountAddressForChain(chainCaip2Id?: ChainCaip2Id): Address | undefined { - return getAccountAddressForChain(chainCaip2Id, useAccountForChain(chainCaip2Id)); +export function useAccountAddressForChain(chainName?: ChainName): Address | undefined { + return getAccountAddressForChain(chainName, useAccounts().accounts); } 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; + return account?.addresses[0]?.address; } } +export function getAccountAddressAndPubKey( + chainName?: ChainName, + accounts?: Record, +): { address?: Address; publicKey?: Promise } { + const address = getAccountAddressForChain(chainName, accounts); + if (!accounts || !chainName || !address) return {}; + const protocol = getChainProtocol(chainName); + const publicKey = accounts[protocol]?.publicKey; + return { address, publicKey }; +} + export function useConnectFns(): Record void> { const onConnectEthereum = useEvmConnectFn(); const onConnectSolana = useSolConnectFn(); diff --git a/src/features/wallet/hooks/solana.ts b/src/features/wallet/hooks/solana.ts index 0ca5ab02..23b9d95e 100644 --- a/src/features/wallet/hooks/solana.ts +++ b/src/features/wallet/hooks/solana.ts @@ -6,10 +6,9 @@ import { toast } from 'react-toastify'; import { ProtocolType } from '@hyperlane-xyz/utils'; +import { getMultiProvider } from '../../../context/context'; import { logger } from '../../../utils/logger'; -import { getCaip2Id, getChainReference } from '../../caip/chains'; import { getChainByRpcEndpoint } from '../../chains/utils'; -import { getChainMetadata, getMultiProvider } from '../../multiProvider'; import { AccountInfo, ActiveChainInfo, ChainTransactionFns } from './types'; @@ -48,7 +47,7 @@ export function useSolActiveChain(): ActiveChainInfo { if (!metadata) return {}; return { chainDisplayName: metadata.displayName, - chainCaip2Id: getCaip2Id(ProtocolType.Sealevel, metadata.chainId), + chainName: metadata.name, }; }, [connectionEndpoint]); } @@ -56,30 +55,29 @@ export function useSolActiveChain(): ActiveChainInfo { export function useSolTransactionFns(): ChainTransactionFns { const { sendTransaction: sendSolTransaction } = useWallet(); - const onSwitchNetwork = useCallback(async (chainCaip2Id: ChainCaip2Id) => { - const chainName = getChainMetadata(chainCaip2Id).displayName; + const onSwitchNetwork = useCallback(async (chainName: ChainName) => { toast.warn(`Solana wallet must be connected to origin chain ${chainName}}`); }, []); const onSendTx = useCallback( async ({ tx, - chainCaip2Id, - activeCap2Id, + chainName, + activeChainName, }: { tx: Transaction; - chainCaip2Id: ChainCaip2Id; - activeCap2Id?: ChainCaip2Id; + chainName: ChainName; + activeChainName?: ChainName; }) => { - if (activeCap2Id && activeCap2Id !== chainCaip2Id) await onSwitchNetwork(chainCaip2Id); - const rpcUrl = getMultiProvider().getRpcUrl(getChainReference(chainCaip2Id)); + if (activeChainName && activeChainName !== chainName) await onSwitchNetwork(chainName); + const rpcUrl = getMultiProvider().getRpcUrl(chainName); const connection = new Connection(rpcUrl, 'confirmed'); const { context: { slot: minContextSlot }, value: { blockhash, lastValidBlockHeight }, } = await connection.getLatestBlockhashAndContext(); - logger.debug(`Sending tx on chain ${chainCaip2Id}`); + logger.debug(`Sending tx on chain ${chainName}`); const signature = await sendSolTransaction(tx, connection, { minContextSlot }); const confirm = () => diff --git a/src/features/wallet/hooks/types.ts b/src/features/wallet/hooks/types.ts index 79db3d2f..cd9e9b0a 100644 --- a/src/features/wallet/hooks/types.ts +++ b/src/features/wallet/hooks/types.ts @@ -1,8 +1,9 @@ -import { ProtocolType } from '@hyperlane-xyz/utils'; +import { ProviderType } from '@hyperlane-xyz/sdk'; +import { HexString, ProtocolType } from '@hyperlane-xyz/utils'; export interface ChainAddress { address: string; - chainCaip2Id?: ChainCaip2Id; + chainName?: ChainName; } export interface AccountInfo { @@ -10,22 +11,26 @@ export interface AccountInfo { // This needs to be an array instead of a single address b.c. // Cosmos wallets have different addresses per chain addresses: Array; + // And another Cosmos exception, public keys are needed + // for tx simulation and gas estimation + publicKey?: Promise; connectorName?: string; isReady: boolean; } export interface ActiveChainInfo { chainDisplayName?: string; - chainCaip2Id?: ChainCaip2Id; + chainName?: ChainName; } export type SendTransactionFn = (params: { tx: TxReq; - chainCaip2Id: ChainCaip2Id; - activeCap2Id?: ChainCaip2Id; + chainName: ChainName; + activeChainName?: ChainName; + providerType?: ProviderType; }) => Promise<{ hash: string; confirm: () => Promise }>; -export type SwitchNetworkFn = (chainCaip2Id: ChainCaip2Id) => Promise; +export type SwitchNetworkFn = (chainName: ChainName) => Promise; export interface ChainTransactionFns { sendTransaction: SendTransactionFn; diff --git a/src/global.d.ts b/src/global.d.ts index 5a50cf27..74fc0ac9 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,7 +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/scripts/buildConfigs/build.sh b/src/scripts/buildConfigs/build.sh deleted file mode 100755 index 7315b95b..00000000 --- a/src/scripts/buildConfigs/build.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Create placeholder files or ts-node script will fail -echo "{}" > ./src/context/_chains.json -echo "[]" > ./src/context/_tokens.json -echo "{}" > ./src/context/_routes.json - -# Run actual build script -yarn ts-node src/scripts/buildConfigs/index.ts diff --git a/src/scripts/buildConfigs/index.ts b/src/scripts/buildConfigs/index.ts deleted file mode 100644 index 3aa16ecc..00000000 --- a/src/scripts/buildConfigs/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env node -import fs from 'fs'; -import path from 'path'; - -import { MultiProtocolProvider } from '@hyperlane-xyz/sdk'; - -import { type WarpContext, setWarpContext } from '../../context/context'; -import { logger } from '../../utils/logger'; - -import { getProcessedChainConfigs } from './chains'; -import { getRouteConfigs } from './routes'; -import { getProcessedTokenConfigs } from './tokens'; - -const CHAINS_OUT_PATH = path.resolve(__dirname, '../../context/_chains.json'); -const TOKENS_OUT_PATH = path.resolve(__dirname, '../../context/_tokens.json'); -const ROUTES_OUT_PATH = path.resolve(__dirname, '../../context/_routes.json'); - -async function main() { - logger.info('Getting chains'); - const chains = getProcessedChainConfigs(); - const multiProvider = new MultiProtocolProvider<{ mailbox?: Address }>(chains); - logger.info('Getting tokens'); - const tokens = await getProcessedTokenConfigs(multiProvider); - - const context: WarpContext = { - chains, - tokens, - routes: {}, - multiProvider, - }; - setWarpContext(context); - - logger.info('Getting routes'); - const routes = await getRouteConfigs(context); - - logger.info(`Writing chains to file ${CHAINS_OUT_PATH}`); - fs.writeFileSync(CHAINS_OUT_PATH, JSON.stringify(chains, null, 2), 'utf8'); - logger.info(`Writing tokens to file ${TOKENS_OUT_PATH}`); - fs.writeFileSync(TOKENS_OUT_PATH, JSON.stringify(tokens, null, 2), 'utf8'); - logger.info(`Writing routes to file ${ROUTES_OUT_PATH}`); - fs.writeFileSync(ROUTES_OUT_PATH, JSON.stringify(routes, null, 2), 'utf8'); -} - -main() - .then(() => logger.info('Done processing configs')) - .catch((error) => logger.warn('Error processing configs', error)); diff --git a/src/scripts/buildConfigs/routes.test.ts b/src/scripts/buildConfigs/routes.test.ts deleted file mode 100644 index 7fd7a01d..00000000 --- a/src/scripts/buildConfigs/routes.test.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { TokenType } from '@hyperlane-xyz/sdk'; - -import { SOL_ZERO_ADDRESS } from '../../consts/values'; - -import { computeTokenRoutes } from './routes'; - -describe('computeTokenRoutes', () => { - it('Handles empty list', () => { - const routesMap = computeTokenRoutes([]); - expect(routesMap).toBeTruthy(); - expect(Object.values(routesMap).length).toBe(0); - }); - - it('Handles basic 3-node route', () => { - const routesMap = computeTokenRoutes([ - { - type: TokenType.collateral, - tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - name: 'Weth', - symbol: 'WETH', - decimals: 18, - hypTokens: [ - { - decimals: 18, - chain: 'ethereum:11155111', - router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - }, - { - decimals: 18, - chain: 'ethereum:44787', - router: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', - }, - ], - }, - ]); - expect(routesMap).toEqual({ - 'ethereum:5': { - 'ethereum:11155111': [ - { - type: 'collateralToSynthetic', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:5', - originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originDecimals: 18, - destCaip2Id: 'ethereum:11155111', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - destDecimals: 18, - }, - ], - 'ethereum:44787': [ - { - type: 'collateralToSynthetic', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:5', - originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originDecimals: 18, - destCaip2Id: 'ethereum:44787', - destRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', - destDecimals: 18, - }, - ], - }, - 'ethereum:11155111': { - 'ethereum:5': [ - { - type: 'syntheticToCollateral', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:11155111', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - originDecimals: 18, - destCaip2Id: 'ethereum:5', - destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - destDecimals: 18, - }, - ], - 'ethereum:44787': [ - { - type: 'syntheticToSynthetic', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:11155111', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - originDecimals: 18, - destCaip2Id: 'ethereum:44787', - destRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', - destDecimals: 18, - }, - ], - }, - 'ethereum:44787': { - 'ethereum:5': [ - { - type: 'syntheticToCollateral', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:44787', - originRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', - originDecimals: 18, - destCaip2Id: 'ethereum:5', - destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - destDecimals: 18, - }, - ], - 'ethereum:11155111': [ - { - type: 'syntheticToSynthetic', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:44787', - originRouterAddress: '0xEcbc0faAA269Cf649AC8950838664BB7B355BD6C', - originDecimals: 18, - destCaip2Id: 'ethereum:11155111', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - destDecimals: 18, - }, - ], - }, - }); - }); - - it('Handles multi-collateral route', () => { - const routesMap = computeTokenRoutes([ - { - type: TokenType.collateral, - tokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - routerAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - name: 'Weth', - symbol: 'WETH', - decimals: 18, - hypTokens: [ - { - decimals: 18, - chain: 'ethereum:11155111', - router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - }, - { - decimals: 6, - chain: 'sealevel:1399811151', - router: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - }, - ], - }, - { - type: TokenType.native, - tokenCaip19Id: `sealevel:1399811151/native:${SOL_ZERO_ADDRESS}`, - routerAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - name: 'Zebec', - symbol: 'ZBC', - decimals: 6, - hypTokens: [ - { - decimals: 18, - chain: 'ethereum:11155111', - router: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - }, - { - decimals: 18, - chain: 'ethereum:5', - router: '0x145de8760021c4ac6676376691b78038d3DE9097', - }, - ], - }, - ]); - expect(routesMap).toEqual({ - 'ethereum:5': { - 'ethereum:11155111': [ - { - type: 'collateralToSynthetic', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:5', - originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originDecimals: 18, - destCaip2Id: 'ethereum:11155111', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - destDecimals: 18, - }, - ], - 'sealevel:1399811151': [ - { - type: 'collateralToCollateral', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:5', - originRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originDecimals: 18, - destCaip2Id: 'sealevel:1399811151', - destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - destDecimals: 6, - destTokenCaip19Id: - 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', - }, - ], - }, - 'ethereum:11155111': { - 'ethereum:5': [ - { - type: 'syntheticToCollateral', - baseTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - baseRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - originCaip2Id: 'ethereum:11155111', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - originDecimals: 18, - destCaip2Id: 'ethereum:5', - destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - destDecimals: 18, - }, - ], - 'sealevel:1399811151': [ - { - type: 'syntheticToCollateral', - baseTokenCaip19Id: - 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', - baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - originCaip2Id: 'ethereum:11155111', - originRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - originDecimals: 18, - destCaip2Id: 'sealevel:1399811151', - destRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - destDecimals: 6, - }, - ], - }, - 'sealevel:1399811151': { - 'ethereum:5': [ - { - type: 'collateralToCollateral', - baseTokenCaip19Id: - 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', - baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - originCaip2Id: 'sealevel:1399811151', - originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - originDecimals: 6, - destCaip2Id: 'ethereum:5', - destRouterAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', - destDecimals: 18, - destTokenCaip19Id: 'ethereum:5/erc20:0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', - }, - ], - 'ethereum:11155111': [ - { - type: 'collateralToSynthetic', - baseTokenCaip19Id: - 'sealevel:1399811151/native:00000000000000000000000000000000000000000000', - baseRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - originCaip2Id: 'sealevel:1399811151', - originRouterAddress: 'PJH5QAbxAqrrnSXfH3GHR8icua8CDFZmo97z91xmpvx', - originDecimals: 6, - destCaip2Id: 'ethereum:11155111', - destRouterAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', - destDecimals: 18, - }, - ], - }, - }); - }); -}); diff --git a/src/scripts/buildConfigs/routes.ts b/src/scripts/buildConfigs/routes.ts deleted file mode 100644 index 9bed8754..00000000 --- a/src/scripts/buildConfigs/routes.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { ProtocolType, bytesToProtocolAddress, deepCopy, eqAddress } from '@hyperlane-xyz/utils'; - -import { ibcRoutes } from '../../consts/ibcRoutes'; -import { WarpContext } from '../../context/context'; -import { getCaip2Id } from '../../features/caip/chains'; -import { getChainIdFromToken, isNonFungibleToken } from '../../features/caip/tokens'; -import { Route, RouteType, RoutesMap } from '../../features/routes/types'; -import { AdapterFactory } from '../../features/tokens/AdapterFactory'; -import { isIbcToken } from '../../features/tokens/metadata'; -import { TokenMetadata, TokenMetadataWithHypTokens } from '../../features/tokens/types'; -import { logger } from '../../utils/logger'; - -export async function getRouteConfigs(context: WarpContext): Promise { - logger.info('Searching for token routes'); - const processedTokens: TokenMetadataWithHypTokens[] = []; - for (const token of context.tokens) { - // Skip querying of IBC tokens - if (isIbcToken(token)) continue; - const tokenWithHypTokens = await fetchRemoteHypTokens(context, token); - processedTokens.push(tokenWithHypTokens); - } - let routes = computeTokenRoutes(processedTokens); - - if (ibcRoutes) { - logger.info('Found ibc route configs, adding to route map'); - routes = mergeRoutes(routes, ibcRoutes); - } - - logger.info('Done searching for token routes'); - return routes; -} - -export async function fetchRemoteHypTokens( - context: WarpContext, - baseToken: TokenMetadata, -): Promise { - const { - symbol: baseSymbol, - tokenCaip19Id: baseTokenCaip19Id, - routerAddress: baseRouter, - } = baseToken; - const isNft = isNonFungibleToken(baseTokenCaip19Id); - logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseTokenCaip19Id})`); - - const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter); - - const remoteRouters = await baseAdapter.getAllRouters(); - logger.info(`Router addresses found:`, remoteRouters.length); - - const hypTokens = await Promise.all( - remoteRouters.map(async (router) => { - const remoteMetadata = context.multiProvider.getChainMetadata(router.domain); - const protocol = remoteMetadata.protocol || ProtocolType.Ethereum; - const chain = getCaip2Id(protocol, context.multiProvider.getChainId(router.domain)); - const formattedAddress = bytesToProtocolAddress( - router.address, - protocol, - remoteMetadata.bech32Prefix, - ); - if (isNft) return { chain, router: formattedAddress, decimals: 0 }; - - const routerDecimals = await getRemoteRouterDecimals( - context, - formattedAddress, - chain, - baseTokenCaip19Id, - baseToken.decimals, - ); - return { chain, router: formattedAddress, decimals: routerDecimals }; - }), - ); - return { ...baseToken, hypTokens }; -} - -async function getRemoteRouterDecimals( - context: WarpContext, - router: Address, - chain: ChainCaip2Id, - baseToken: TokenCaip19Id, - originDecimals: number, -) { - // Attempt to find the decimals from the token list - const routerMetadata = context.tokens.find((token) => eqAddress(router, token.routerAddress)); - if (routerMetadata) return routerMetadata.decimals; - - // Otherwise try to query the contract - try { - const remoteAdapter = AdapterFactory.HypSyntheticTokenAdapterFromAddress( - baseToken, - chain, - router, - ); - const metadata = await remoteAdapter.getMetadata(); - return metadata.decimals; - } catch (error) { - logger.warn(`Failed to get metadata for router ${router} on chain ${chain}`); - } - - // Fallback to using origin router's decimals - logger.warn('Falling back to origin decimals', originDecimals); - return originDecimals; -} - -// Process token list to populates routesCache with all possible token routes (e.g. router pairs) -export function computeTokenRoutes(tokens: TokenMetadataWithHypTokens[]): RoutesMap { - const tokenRoutes: RoutesMap = {}; - - // Instantiate map structure - const allChainIds = getChainsFromTokens(tokens); - for (const origin of allChainIds) { - tokenRoutes[origin] = {}; - for (const dest of allChainIds) { - if (origin === dest) continue; - tokenRoutes[origin][dest] = []; - } - } - - // Compute all possible routes, in both directions - for (const token of tokens) { - for (const remoteHypToken of token.hypTokens) { - const { - tokenCaip19Id: baseTokenCaip19Id, - routerAddress: baseRouterAddress, - decimals: baseDecimals, - } = token; - const baseChainCaip2Id = getChainIdFromToken(baseTokenCaip19Id); - const { - chain: remoteChainCaip2Id, - router: remoteRouterAddress, - decimals: remoteDecimals, - } = remoteHypToken; - // Check if the token list contains the dest router address, meaning it's also a base collateral token - const remoteBaseTokenConfig = findTokenByRouter(tokens, remoteRouterAddress); - const commonRouteProps = { baseTokenCaip19Id, baseRouterAddress }; - - // Register a route from the base to the remote - tokenRoutes[baseChainCaip2Id][remoteChainCaip2Id]?.push({ - type: remoteBaseTokenConfig - ? RouteType.CollateralToCollateral - : RouteType.CollateralToSynthetic, - ...commonRouteProps, - originCaip2Id: baseChainCaip2Id, - originRouterAddress: baseRouterAddress, - originDecimals: baseDecimals, - destCaip2Id: remoteChainCaip2Id, - destRouterAddress: remoteRouterAddress, - destDecimals: remoteDecimals, - destTokenCaip19Id: remoteBaseTokenConfig ? remoteBaseTokenConfig.tokenCaip19Id : undefined, - }); - - // If the remote is not a synthetic (i.e. it's a native/collateral token with it's own config) - // then stop here to avoid duplicate route entries. - if (remoteBaseTokenConfig) continue; - - // Register a route back from the synthetic remote to the base - tokenRoutes[remoteChainCaip2Id][baseChainCaip2Id]?.push({ - type: RouteType.SyntheticToCollateral, - ...commonRouteProps, - originCaip2Id: remoteChainCaip2Id, - originRouterAddress: remoteRouterAddress, - originDecimals: remoteDecimals, - destCaip2Id: baseChainCaip2Id, - destRouterAddress: baseRouterAddress, - destDecimals: baseDecimals, - }); - - // Now create routes from the remote synthetic token to all other hypTokens - // This assumes the synthetics were all enrolled to connect to each other - // which is the deployer's default behavior - for (const otherHypToken of token.hypTokens) { - const { chain: otherSynCaip2Id, router: otherHypTokenAddress } = otherHypToken; - // Skip if it's same hypToken as parent loop (no route to self) - if (otherHypToken === remoteHypToken) continue; - // Also skip if remote isn't a synthetic (i.e. has a collateral/native config) - if (findTokenByRouter(tokens, otherHypTokenAddress)) continue; - - tokenRoutes[remoteChainCaip2Id][otherSynCaip2Id]?.push({ - type: RouteType.SyntheticToSynthetic, - ...commonRouteProps, - originCaip2Id: remoteChainCaip2Id, - originRouterAddress: remoteRouterAddress, - originDecimals: remoteDecimals, - destCaip2Id: otherSynCaip2Id, - destRouterAddress: otherHypTokenAddress, - destDecimals: otherHypToken.decimals, - }); - } - } - } - return tokenRoutes; -} - -function getChainsFromTokens(tokens: TokenMetadataWithHypTokens[]): ChainCaip2Id[] { - const chains = new Set(); - for (const token of tokens) { - chains.add(getChainIdFromToken(token.tokenCaip19Id)); - for (const hypToken of token.hypTokens) { - chains.add(hypToken.chain); - } - } - return Array.from(chains); -} - -function findTokenByRouter(tokens: TokenMetadataWithHypTokens[], router: Address) { - return tokens.find((t) => eqAddress(t.routerAddress, router)); -} - -export function mergeRoutes(routes: RoutesMap, newRoutes: Route[]) { - const mergedRoutes = deepCopy(routes); - for (const route of newRoutes) { - mergedRoutes[route.originCaip2Id] ||= {}; - mergedRoutes[route.originCaip2Id][route.destCaip2Id] ||= []; - mergedRoutes[route.originCaip2Id][route.destCaip2Id].push(route); - } - return mergedRoutes; -} diff --git a/src/scripts/buildConfigs/tokens.ts b/src/scripts/buildConfigs/tokens.ts deleted file mode 100644 index 86a37978..00000000 --- a/src/scripts/buildConfigs/tokens.ts +++ /dev/null @@ -1,132 +0,0 @@ -import path from 'path'; - -import { - EvmTokenAdapter, - ITokenAdapter, - MultiProtocolProvider, - TokenType, -} from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; - -import TokensJson from '../../consts/tokens.json'; -import { tokenList as TokensTS } from '../../consts/tokens.ts'; -import { getCaip2Id } from '../../features/caip/chains'; -import { - getCaip19Id, - getNativeTokenAddress, - resolveAssetNamespace, -} from '../../features/caip/tokens'; -import { getHypErc20CollateralContract } from '../../features/tokens/contracts/evmContracts'; -import { - MinimalTokenMetadata, - TokenMetadata, - WarpTokenConfig, - WarpTokenConfigSchema, -} from '../../features/tokens/types'; -import { logger } from '../../utils/logger'; - -import { readYaml } from './utils'; - -export async function getProcessedTokenConfigs(multiProvider: MultiProtocolProvider) { - const TokensYaml = readYaml(path.resolve(__dirname, '../../consts/tokens.yaml')); - const tokenList = [...TokensJson, ...TokensYaml, ...TokensTS]; - const tokens = await parseTokenConfigs(multiProvider, tokenList); - return tokens; -} - -// Converts the more user-friendly config format into a validated, extended format -// that's easier for the UI to work with -async function parseTokenConfigs( - multiProvider: MultiProtocolProvider, - configList: WarpTokenConfig, -): Promise { - const result = WarpTokenConfigSchema.safeParse(configList); - if (!result.success) { - logger.warn('Invalid token config', result.error); - throw new Error(`Invalid token config: ${result.error.toString()}`); - } - - const parsedConfig = result.data; - const tokenMetadata: TokenMetadata[] = []; - for (const config of parsedConfig) { - const { type, chainId, logoURI, igpTokenAddressOrDenom } = config; - - const protocol = multiProvider.getChainMetadata(chainId).protocol || ProtocolType.Ethereum; - const chainCaip2Id = getCaip2Id(protocol, chainId); - const isNative = type == TokenType.native; - const isNft = type === TokenType.collateral && config.isNft; - const isSpl2022 = type === TokenType.collateral && config.isSpl2022; - const address = - type === TokenType.collateral ? config.address : getNativeTokenAddress(protocol); - const routerAddress = - type === TokenType.collateral - ? config.hypCollateralAddress - : type === TokenType.native - ? config.hypNativeAddress - : ''; - const namespace = resolveAssetNamespace(protocol, isNative, isNft, isSpl2022); - const tokenCaip19Id = getCaip19Id(chainCaip2Id, namespace, address); - - const { name, symbol, decimals } = await fetchNameAndDecimals( - multiProvider, - config, - protocol, - routerAddress, - isNft, - ); - - tokenMetadata.push({ - name, - symbol, - decimals, - logoURI, - type, - tokenCaip19Id, - routerAddress, - igpTokenAddressOrDenom, - }); - } - return tokenMetadata; -} - -async function fetchNameAndDecimals( - multiProvider: MultiProtocolProvider, - tokenConfig: WarpTokenConfig[number], - protocol: ProtocolType, - routerAddress: Address, - isNft?: boolean, -): Promise { - const { type, chainId, name, symbol, decimals } = tokenConfig; - if (name && symbol && decimals) { - // Already provided in the config - return { name, symbol, decimals }; - } - - const chainMetadata = multiProvider.getChainMetadata(chainId); - - if (type === TokenType.native) { - // Use the native token config that may be in the chain metadata - const tokenMetadata = chainMetadata.nativeToken; - if (!tokenMetadata) throw new Error('Name, symbol, or decimals is missing for native token'); - return tokenMetadata; - } - - if (type === TokenType.collateral) { - // Fetch the data from the contract - let tokenAdapter: ITokenAdapter; - if (protocol === ProtocolType.Ethereum) { - const provider = multiProvider.getEthersV5Provider(chainId); - const collateralContract = getHypErc20CollateralContract(routerAddress, provider); - const wrappedTokenAddr = await collateralContract.wrappedToken(); - tokenAdapter = new EvmTokenAdapter(chainMetadata.name, multiProvider, { - token: wrappedTokenAddr, - }); - } else { - // TODO solana support when hyp tokens have metadata - throw new Error('Name, symbol, and decimals is required for non-EVM token configs'); - } - return tokenAdapter.getMetadata(isNft); - } - - throw new Error(`Unsupported token type ${type}`); -} diff --git a/src/scripts/buildConfigs/utils.ts b/src/scripts/buildConfigs/utils.ts deleted file mode 100644 index 71e85773..00000000 --- a/src/scripts/buildConfigs/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import fs from 'fs'; -import { parse } from 'yaml'; - -export function readYaml(path: string) { - return parse(fs.readFileSync(path, 'utf-8')); -} diff --git a/src/utils/links.ts b/src/utils/links.ts index e1e17707..88d99fc0 100644 --- a/src/utils/links.ts +++ b/src/utils/links.ts @@ -2,17 +2,14 @@ import { toBase64 } from '@hyperlane-xyz/utils'; import { config } from '../consts/config'; import { links } from '../consts/links'; -import { parseCaip2Id } from '../features/caip/chains'; -import { isPermissionlessChain } from '../features/chains/utils'; -import { getMultiProvider } from '../features/multiProvider'; +import { getChainMetadata, isPermissionlessChain } from '../features/chains/utils'; // TODO test with cosmos chain config, or disallow it -export function getHypExplorerLink(originCaip2Id: ChainCaip2Id, msgId?: string) { - if (!config.enableExplorerLink || !originCaip2Id || !msgId) return null; +export function getHypExplorerLink(chain: ChainName, msgId?: string) { + if (!config.enableExplorerLink || !chain || !msgId) return null; const baseLink = `${links.explorer}/message/${msgId}`; - if (isPermissionlessChain(originCaip2Id)) { - const { reference } = parseCaip2Id(originCaip2Id); - const chainConfig = getMultiProvider().getChainMetadata(reference); + if (isPermissionlessChain(chain)) { + const chainConfig = getChainMetadata(chain); const serializedConfig = toBase64([chainConfig]); if (serializedConfig) { const params = new URLSearchParams({ chains: serializedConfig }); diff --git a/src/utils/transfer.ts b/src/utils/transfer.ts index dbf22818..90dceae0 100644 --- a/src/utils/transfer.ts +++ b/src/utils/transfer.ts @@ -18,14 +18,11 @@ export function getTransferStatusLabel( statusDescription = 'Please connect wallet to continue'; else if (status === TransferStatus.Preparing) statusDescription = 'Preparing for token transfer...'; - else if (status === TransferStatus.CreatingApprove) - statusDescription = 'Preparing approve transaction...'; + else if (status === TransferStatus.CreatingTxs) statusDescription = 'Creating transactions...'; else if (status === TransferStatus.SigningApprove) statusDescription = `Sign approve transaction in ${connectorName} to continue.`; else if (status === TransferStatus.ConfirmingApprove) statusDescription = 'Confirming approve transaction...'; - else if (status === TransferStatus.CreatingTransfer) - statusDescription = 'Preparing transfer transaction...'; else if (status === TransferStatus.SigningTransfer) statusDescription = `Sign transfer transaction in ${connectorName} to continue.`; else if (status === TransferStatus.ConfirmingTransfer) 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; + } +} diff --git a/yarn.lock b/yarn.lock index 70e05be8..4413a992 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,30 +3014,30 @@ __metadata: languageName: node linkType: hard -"@hyperlane-xyz/core@npm:3.6.1": - version: 3.6.1 - resolution: "@hyperlane-xyz/core@npm:3.6.1" +"@hyperlane-xyz/core@npm:3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/core@npm:3.8.0" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/utils": "npm:3.8.0" "@openzeppelin/contracts": "npm:^4.9.3" "@openzeppelin/contracts-upgradeable": "npm:^v4.9.3" peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" "@types/sinon-chai": "*" - checksum: 15cf69663a6f80ef8c656f283a2af84c9be58cb2a84f88820cda5b6233157aaac69557b97983ad4138078b0dc0a77cb37f6bc992d4ba8bf4b5e512cbf942526f + checksum: f0f614bd1a1d8a755d8522409473b5cb3042304450e3ffb8ac96cd2756ca27b9a6f0a243608ffddf70a31af3b1e8dba0138154615c41424b2fb2e5baca52c963 languageName: node linkType: hard -"@hyperlane-xyz/sdk@npm:^3.6.1": - version: 3.6.1 - resolution: "@hyperlane-xyz/sdk@npm:3.6.1" +"@hyperlane-xyz/sdk@npm:^3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/sdk@npm:3.8.0" dependencies: "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/core": "npm:3.8.0" + "@hyperlane-xyz/utils": "npm:3.8.0" "@solana/spl-token": "npm:^0.3.8" "@solana/web3.js": "npm:^1.78.0" "@types/coingecko-api": "npm:^1.0.10" @@ -3054,19 +3054,19 @@ __metadata: peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" - checksum: 9c5b0cd9c44ff8f8193740cd895a87c84990d9466ffd695a35f1a0f71dafb8dee2ac294295df3be2849f520ef4ec236cc641f98eb93369a67d2727a75249c7cb + checksum: 5ca551b639a3a5a92266adbac9da973dd417e8a399797c2449b07af15c0f1f4659d1b98f4c1b834db999db476f66b832db4eac37efa1b9f50bc6c2530b2f98fd languageName: node linkType: hard -"@hyperlane-xyz/utils@npm:3.6.1, @hyperlane-xyz/utils@npm:^3.6.1": - version: 3.6.1 - resolution: "@hyperlane-xyz/utils@npm:3.6.1" +"@hyperlane-xyz/utils@npm:3.8.0, @hyperlane-xyz/utils@npm:^3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/utils@npm:3.8.0" dependencies: "@cosmjs/encoding": "npm:^0.31.3" "@solana/web3.js": "npm:^1.78.0" bignumber.js: "npm:^9.1.1" ethers: "npm:^5.7.2" - checksum: dca6656ba047fac0ab8e8e9cc4687cdc1784d2245efa57bf4826f35eac6fa0f2c6fb9834f1dc3b699ce3cb0b5ccfd02940d7eb3b574204d8c2b4f4778aa6d301 + checksum: 9d313133d3cc0cdae605c96ffdcebb704a5951a75201bc23be8a2653fbad45e0a1fd4782f8d93ec0b5a01bddaa98999ef5f320cfbb6eef44e0de2176a8a4fb7b languageName: node linkType: hard @@ -3086,9 +3086,9 @@ __metadata: "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@headlessui/react": "npm:^1.7.14" - "@hyperlane-xyz/sdk": "npm:^3.6.1" - "@hyperlane-xyz/utils": "npm:^3.6.1" - "@hyperlane-xyz/widgets": "npm:^3.1.4" + "@hyperlane-xyz/sdk": "npm:^3.8.0" + "@hyperlane-xyz/utils": "npm:^3.8.0" + "@hyperlane-xyz/widgets": "npm:^3.8.0" "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28974b4e81129bfbe3cab76308b889032a6" "@next/bundle-analyzer": "npm:^14.0.4" "@rainbow-me/rainbowkit": "npm:1.3.0" @@ -3137,14 +3137,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/widgets@npm:^3.1.4": - version: 3.1.4 - resolution: "@hyperlane-xyz/widgets@npm:3.1.4" +"@hyperlane-xyz/widgets@npm:^3.8.0": + version: 3.8.0 + resolution: "@hyperlane-xyz/widgets@npm:3.8.0" peerDependencies: "@hyperlane-xyz/sdk": ^3.1 react: ^18 react-dom: ^18 - checksum: 0183bdb11015d07bdd92be033e658dbcd614cece361654a00d63e3fe8c81c962d9f7fa0e9e51ab0affc59b1c0c94da7c433794ae0e7ca4a0e6d77f42f4a0744c + checksum: 2a36a90d43250c86084b05580909f316f13ed37a9416feea4e20411cd16ca42d430c5746636abfc9eb948199f888cf2f02b1e4d2a92dbc7fa4eee24af0f13579 languageName: node linkType: hard @@ -14056,20 +14056,13 @@ __metadata: languageName: node linkType: hard -"react-fast-compare@npm:3.2.2": +"react-fast-compare@npm:^3.2": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" checksum: a6826180ba75cefba1c8d3ac539735f9b627ca05d3d307fe155487f5d0228d376dac6c9708d04a283a7b9f9aee599b637446635b79c8c8753d0b4eece56c125c languageName: node linkType: hard -"react-fast-compare@npm:^2.0.1": - version: 2.0.4 - resolution: "react-fast-compare@npm:2.0.4" - checksum: e4e3218c0f5c29b88e9f184a12adb77b0a93a803dbd45cb98bbb754c8310dc74e6266c53dd70b90ba4d0939e0e1b8a182cb05d081bcab22507a0390fbcd768ac - languageName: node - linkType: hard - "react-focus-lock@npm:^2.9.4": version: 2.9.6 resolution: "react-focus-lock@npm:2.9.6"