Migration to warp core (#134)
Migrate to WarpCore * Upgrade to 3.8.0 * Form bug fixes * Fix cosmos wallet hook log Local gas estimation and smart max button (#139) * Implementing new useFeeQuote hook * Implement smarter max button * Finish smart max computation and gas handling * Improve form loading statespull/152/head
parent
d01d616e77
commit
6127398720
@ -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<ComponentProps<typeof ChainLogoInner>, 'chainId' | 'chainName'> & { |
||||
chainCaip2Id?: ChainCaip2Id; |
||||
}; |
||||
export function ChainLogo(props: ComponentProps<typeof ChainLogoInner>) { |
||||
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 }) => ( |
||||
<Image src={logoUri} alt="" {...props} /> |
||||
) |
||||
: 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 }) => ( |
||||
<Image src={logoUri} alt="" {...props} /> |
||||
) |
||||
: undefined; |
||||
return { |
||||
chainId, |
||||
chainName, |
||||
icon, |
||||
}; |
||||
} catch (error) { |
||||
logger.error('Failed to parse caip2 id', error); |
||||
return {}; |
||||
} |
||||
}, [chainCaip2Id]); |
||||
|
||||
return <ChainLogoInner {...rest} chainId={chainId} chainName={chainName} icon={icon} />; |
||||
return <ChainLogoInner {...rest} chainId={chainId} chainName={chainDisplayName} icon={icon} />; |
||||
} |
||||
|
@ -1,4 +0,0 @@ |
||||
import { IbcRoute, IbcToWarpRoute } from '../features/routes/types'; |
||||
|
||||
// Configs for manually-defined IBC-only routes
|
||||
export const ibcRoutes: Array<IbcRoute | IbcToWarpRoute> = []; |
@ -1,11 +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<chainId,value>
|
||||
export const DEFAULT_IGP_QUOTES: Partial< |
||||
Record<ProtocolType, string | Record<string | number, string>> |
||||
> = { |
||||
[ProtocolType.Sealevel]: '10000', |
||||
[ProtocolType.Cosmos]: '270000', |
||||
}; |
@ -1 +1,4 @@ |
||||
[] |
||||
{ |
||||
"tokens": [], |
||||
"options": {} |
||||
} |
||||
|
@ -1,30 +1,9 @@ |
||||
import { WarpTokenConfig } from '../features/tokens/types'; |
||||
import { WarpCoreConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
// A list of Warp UI token configs
|
||||
// Tokens can be defined here, in tokens.json, or in tokens.yaml
|
||||
// The input here is typically the output of the Hyperlane CLI warp deploy command
|
||||
export const tokenList: WarpTokenConfig = [ |
||||
// Example collateral token for an EVM chain
|
||||
{ |
||||
type: 'collateral', |
||||
chainId: 5, |
||||
address: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', |
||||
hypCollateralAddress: '0x145de8760021c4ac6676376691b78038d3DE9097', |
||||
name: 'Weth', |
||||
symbol: 'WETH', |
||||
decimals: 18, |
||||
logoURI: '/logos/weth.png', // See public/logos/
|
||||
}, |
||||
|
||||
// Example NFT (ERC721) token for an EVM chain
|
||||
{ |
||||
chainId: 5, |
||||
name: 'Test721', |
||||
symbol: 'TEST721', |
||||
decimals: 0, |
||||
type: 'collateral', |
||||
address: '0x77566D540d1E207dFf8DA205ed78750F9a1e7c55', |
||||
hypCollateralAddress: '0xDcbc0faAA269Cf649AC8950838664BB7B355BD6B', |
||||
isNft: true, |
||||
}, |
||||
]; |
||||
export const tokenConfigs: WarpCoreConfig = { |
||||
tokens: [], |
||||
options: {}, |
||||
}; |
||||
|
@ -1,14 +1,24 @@ |
||||
# A list of Warp UI token configs |
||||
# Tokens can be defined here, in tokens.json, or in tokens.ts |
||||
# A list of Warp UI token configs and other options for the WarpCore |
||||
# Configs can be defined here, in tokens.json, or in tokens.ts |
||||
# The input here is typically the output of the Hyperlane CLI warp deploy command |
||||
--- |
||||
# Replace this [] with your token list |
||||
[] |
||||
# Example using a native token: |
||||
# - type: native |
||||
# chainId: 11155111 |
||||
# name: 'Ether' |
||||
# symbol: 'ETH' |
||||
# decimals: 18 |
||||
# hypNativeAddress: '0xEa44A29da87B5464774978e6A4F4072A4c048949' |
||||
# logoURI: '/logos/weth.png' |
||||
tokens: |
||||
# Eth Mainnet HypNative token |
||||
- chainName: sepolia |
||||
standard: EvmHypNative |
||||
decimals: 18 |
||||
symbol: ETH |
||||
name: Ether |
||||
addressOrDenom: '0x767C51a91CC9dEF2F24C35c340649411D6390320' |
||||
logoURI: '/logos/weth.png' |
||||
connections: |
||||
- { token: ethereum|alfajores|0x8bF6Ca2Dca1DF703Cb9144cef6A4d86abA7776C4 } |
||||
- chainName: alfajores |
||||
standard: EvmHypSynthetic |
||||
decimals: 18 |
||||
symbol: ETH |
||||
name: Ether |
||||
addressOrDenom: '0x8bF6Ca2Dca1DF703Cb9144cef6A4d86abA7776C4' |
||||
logoURI: '/logos/weth.png' |
||||
connections: |
||||
- { token: ethereum|sepolia|0x767C51a91CC9dEF2F24C35c340649411D6390320 } |
||||
|
@ -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. |
@ -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, |
@ -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 }; |
||||
} |
@ -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); |
||||
} |
@ -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}`); |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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]); |
||||
} |
@ -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<ChainCaip2Id, Record<ChainCaip2Id, Route[]>>; |
@ -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; |
||||
} |
@ -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}`); |
||||
} |
||||
} |
||||
} |
@ -1,232 +1,45 @@ |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { eqAddress, isValidAddress } from '@hyperlane-xyz/utils'; |
||||
import { IToken } from '@hyperlane-xyz/sdk'; |
||||
import { isValidAddress } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { useToastError } from '../../components/toast/useToastError'; |
||||
import { logger } from '../../utils/logger'; |
||||
import { getProtocolType } from '../caip/chains'; |
||||
import { parseCaip19Id, tryGetChainIdFromToken } from '../caip/tokens'; |
||||
import { getEvmProvider } from '../multiProvider'; |
||||
import { RoutesMap } from '../routes/types'; |
||||
import { getTokenRoute, isIbcOnlyRoute, isIbcRoute, isRouteFromNative } from '../routes/utils'; |
||||
import { useStore } from '../store'; |
||||
import { getMultiProvider, getTokenByIndex } from '../../context/context'; |
||||
import { TransferFormValues } from '../transfer/types'; |
||||
import { useAccountAddressForChain } from '../wallet/hooks/multiProtocol'; |
||||
|
||||
import { AdapterFactory } from './AdapterFactory'; |
||||
import { getHypErc721Contract } from './contracts/evmContracts'; |
||||
|
||||
export function useOriginBalance( |
||||
{ originCaip2Id, destinationCaip2Id, tokenCaip19Id }: TransferFormValues, |
||||
tokenRoutes: RoutesMap, |
||||
) { |
||||
const address = useAccountAddressForChain(originCaip2Id); |
||||
const setSenderBalances = useStore((state) => state.setSenderBalances); |
||||
|
||||
export function useBalance(chain?: ChainName, token?: IToken, address?: Address) { |
||||
const { isLoading, isError, error, data } = useQuery({ |
||||
queryKey: [ |
||||
'useOriginBalance', |
||||
address, |
||||
originCaip2Id, |
||||
destinationCaip2Id, |
||||
tokenCaip19Id, |
||||
tokenRoutes, |
||||
], |
||||
queryFn: async () => { |
||||
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); |
||||
const protocol = getProtocolType(originCaip2Id); |
||||
if (!route || !address || !isValidAddress(address, protocol)) return null; |
||||
const tokenAdapter = isIbcRoute(route) |
||||
? AdapterFactory.NativeAdapterFromRoute(route, 'origin') |
||||
: AdapterFactory.HypTokenAdapterFromRouteOrigin(route); |
||||
const tokenBalance = await tokenAdapter.getBalance(address); |
||||
|
||||
let nativeBalance; |
||||
if (isRouteFromNative(route) || isIbcRoute(route)) { |
||||
nativeBalance = tokenBalance; |
||||
} else { |
||||
const nativeAdapter = AdapterFactory.NativeAdapterFromChain(originCaip2Id); |
||||
nativeBalance = await nativeAdapter.getBalance(address); |
||||
} |
||||
|
||||
return { tokenBalance, tokenDecimals: route.originDecimals, nativeBalance }; |
||||
queryKey: ['useBalance', chain, address, token?.addressOrDenom], |
||||
queryFn: () => { |
||||
if (!chain || !token || !address || !isValidAddress(address, token.protocol)) return null; |
||||
return token.getBalance(getMultiProvider(), address); |
||||
}, |
||||
refetchInterval: 5000, |
||||
}); |
||||
|
||||
useToastError(error, 'Error fetching origin balance'); |
||||
|
||||
useEffect(() => { |
||||
setSenderBalances(data?.tokenBalance || '0', data?.nativeBalance || '0'); |
||||
}, [data, setSenderBalances]); |
||||
useToastError(error, 'Error fetching balance'); |
||||
|
||||
return { |
||||
isLoading, |
||||
isError, |
||||
tokenBalance: data?.tokenBalance, |
||||
tokenDecimals: data?.tokenDecimals, |
||||
nativeBalance: data?.nativeBalance, |
||||
balance: data ?? undefined, |
||||
}; |
||||
} |
||||
|
||||
export function useDestinationBalance( |
||||
{ originCaip2Id, destinationCaip2Id, tokenCaip19Id, recipientAddress }: TransferFormValues, |
||||
tokenRoutes: RoutesMap, |
||||
) { |
||||
const { isLoading, isError, error, data } = useQuery({ |
||||
queryKey: [ |
||||
'useDestinationBalance', |
||||
recipientAddress, |
||||
originCaip2Id, |
||||
destinationCaip2Id, |
||||
tokenCaip19Id, |
||||
tokenRoutes, |
||||
], |
||||
queryFn: async () => { |
||||
const route = getTokenRoute(originCaip2Id, destinationCaip2Id, tokenCaip19Id, tokenRoutes); |
||||
const protocol = getProtocolType(destinationCaip2Id); |
||||
if (!route || !recipientAddress || !isValidAddress(recipientAddress, protocol)) return null; |
||||
const tokenAdapter = isIbcOnlyRoute(route) |
||||
? AdapterFactory.NativeAdapterFromRoute(route, 'destination') |
||||
: AdapterFactory.HypTokenAdapterFromRouteDest(route); |
||||
const balance = await tokenAdapter.getBalance(recipientAddress); |
||||
return { balance, decimals: route.destDecimals }; |
||||
}, |
||||
refetchInterval: 5000, |
||||
}); |
||||
|
||||
useToastError(error, 'Error fetching destination balance'); |
||||
|
||||
return { isLoading, isError, balance: data?.balance, decimals: data?.decimals }; |
||||
} |
||||
|
||||
// TODO solana support
|
||||
export function useOriginTokenIdBalance(tokenCaip19Id: TokenCaip19Id) { |
||||
const chainCaip2Id = tryGetChainIdFromToken(tokenCaip19Id); |
||||
const accountAddress = useAccountAddressForChain(chainCaip2Id); |
||||
const setSenderNftIds = useStore((state) => state.setSenderNftIds); |
||||
|
||||
const { |
||||
isLoading, |
||||
isError, |
||||
error, |
||||
data: tokenIds, |
||||
} = useQuery({ |
||||
queryKey: ['useOriginTokenIdBalance', tokenCaip19Id, accountAddress], |
||||
queryFn: () => { |
||||
if (!tokenCaip19Id || !accountAddress) return null; |
||||
return fetchListOfERC721TokenId(tokenCaip19Id, accountAddress); |
||||
}, |
||||
refetchInterval: 5000, |
||||
}); |
||||
|
||||
useToastError(error, 'Error fetching origin token IDs'); |
||||
|
||||
useEffect(() => { |
||||
setSenderNftIds(tokenIds && Array.isArray(tokenIds) ? tokenIds : null); |
||||
}, [tokenIds, setSenderNftIds]); |
||||
|
||||
return { isLoading, isError, tokenIds }; |
||||
} |
||||
|
||||
// TODO solana support
|
||||
export async function fetchListOfERC721TokenId( |
||||
tokenCaip19Id: TokenCaip19Id, |
||||
accountAddress: Address, |
||||
): Promise<string[]> { |
||||
const { chainCaip2Id, address: tokenAddress } = parseCaip19Id(tokenCaip19Id); |
||||
logger.debug(`Fetching list of tokenID for account ${accountAddress} on chain ${chainCaip2Id}`); |
||||
|
||||
const hypERC721 = getHypErc721Contract(tokenAddress, getEvmProvider(chainCaip2Id)); |
||||
|
||||
const balance = await hypERC721.balanceOf(accountAddress); |
||||
const index = Array.from({ length: parseInt(balance.toString()) }, (_, index) => index); |
||||
const promises: Promise<string>[] = index.map(async (id) => { |
||||
const response = await hypERC721.tokenOfOwnerByIndex(accountAddress, id); |
||||
|
||||
return response.toString(); |
||||
}); |
||||
const result = await Promise.all(promises); |
||||
logger.debug(`TokenIds that the ${accountAddress} owns on chain ${chainCaip2Id}: ${result} `); |
||||
return result; |
||||
} |
||||
|
||||
// TODO solana support
|
||||
export function useContractSupportsTokenByOwner( |
||||
tokenCaip19Id: TokenCaip19Id, |
||||
accountAddress?: Address, |
||||
) { |
||||
const { |
||||
isLoading, |
||||
isError, |
||||
error, |
||||
data: isContractAllowToGetTokenIds, |
||||
} = useQuery({ |
||||
queryKey: ['useContractSupportsTokenByOwner', tokenCaip19Id, accountAddress], |
||||
queryFn: () => { |
||||
if (!tokenCaip19Id || !accountAddress) return null; |
||||
return contractSupportsTokenByOwner(tokenCaip19Id, accountAddress); |
||||
}, |
||||
}); |
||||
|
||||
useToastError(error, 'Error ERC721 contract details'); |
||||
|
||||
return { isLoading, isError, isContractAllowToGetTokenIds }; |
||||
} |
||||
|
||||
// TODO solana support
|
||||
async function contractSupportsTokenByOwner( |
||||
tokenCaip19Id: TokenCaip19Id, |
||||
accountAddress: Address, |
||||
): Promise<boolean> { |
||||
const { chainCaip2Id, address: tokenAddress } = parseCaip19Id(tokenCaip19Id); |
||||
const hypERC721 = getHypErc721Contract(tokenAddress, getEvmProvider(chainCaip2Id)); |
||||
try { |
||||
await hypERC721.tokenOfOwnerByIndex(accountAddress, '0'); |
||||
return true; |
||||
} catch (error) { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
// TODO solana support
|
||||
export function useIsSenderNftOwner(tokenCaip19Id: TokenCaip19Id, tokenId: string) { |
||||
const chainCaip2Id = tryGetChainIdFromToken(tokenCaip19Id); |
||||
const senderAddress = useAccountAddressForChain(chainCaip2Id); |
||||
const setIsSenderNftOwner = useStore((state) => state.setIsSenderNftOwner); |
||||
|
||||
const { |
||||
isLoading, |
||||
isError, |
||||
error, |
||||
data: owner, |
||||
} = useQuery({ |
||||
queryKey: ['useOwnerOfErc721', tokenCaip19Id, tokenId], |
||||
queryFn: () => { |
||||
if (!tokenCaip19Id || !tokenId) return null; |
||||
return fetchERC721Owner(tokenCaip19Id, tokenId); |
||||
}, |
||||
}); |
||||
|
||||
useToastError(error, 'Error ERC721 owner'); |
||||
|
||||
useEffect(() => { |
||||
if (!senderAddress || !owner) setIsSenderNftOwner(null); |
||||
else setIsSenderNftOwner(eqAddress(senderAddress, owner)); |
||||
}, [owner, senderAddress, setIsSenderNftOwner]); |
||||
|
||||
return { isLoading, isError, owner }; |
||||
export function useOriginBalance({ origin, tokenIndex }: TransferFormValues) { |
||||
const address = useAccountAddressForChain(origin); |
||||
const token = getTokenByIndex(tokenIndex); |
||||
return useBalance(origin, token, address); |
||||
} |
||||
|
||||
// TODO solana support
|
||||
async function fetchERC721Owner(tokenCaip19Id: TokenCaip19Id, tokenId: string): Promise<string> { |
||||
const { chainCaip2Id, address: tokenAddress } = parseCaip19Id(tokenCaip19Id); |
||||
const hypERC721 = getHypErc721Contract(tokenAddress, getEvmProvider(chainCaip2Id)); |
||||
try { |
||||
const ownerAddress = await hypERC721.ownerOf(tokenId); |
||||
return ownerAddress; |
||||
} catch (error) { |
||||
return ''; |
||||
} |
||||
export function useDestinationBalance({ |
||||
origin, |
||||
destination, |
||||
tokenIndex, |
||||
recipient, |
||||
}: TransferFormValues) { |
||||
const originToken = getTokenByIndex(tokenIndex); |
||||
const connection = originToken?.getConnectionForChain(destination); |
||||
return useBalance(origin, connection?.token, recipient); |
||||
} |
||||
|
@ -1,60 +0,0 @@ |
||||
import { Signer, providers } from 'ethers'; |
||||
|
||||
import { |
||||
ERC20__factory, |
||||
ERC721__factory, |
||||
HypERC20Collateral__factory, |
||||
HypERC20__factory, |
||||
HypERC721Collateral__factory, |
||||
HypERC721__factory, |
||||
HypNative__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
|
||||
export function getHypErc20CollateralContract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return HypERC20Collateral__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getHypErc721CollateralContract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return HypERC721Collateral__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getHypErc20Contract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return HypERC20__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getHypErc721Contract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return HypERC721__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getHypNativeContract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return HypNative__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getErc20Contract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return ERC20__factory.connect(contractAddress, signerOrProvider); |
||||
} |
||||
|
||||
export function getErc721Contract( |
||||
contractAddress: Address, |
||||
signerOrProvider: Signer | providers.Provider, |
||||
) { |
||||
return ERC721__factory.connect(contractAddress, signerOrProvider); |
||||
} |
@ -1,19 +0,0 @@ |
||||
import { getWarpContext } from '../../context/context'; |
||||
|
||||
import { IbcTokenTypes, TokenMetadata } from './types'; |
||||
|
||||
export function getTokens() { |
||||
return getWarpContext()?.tokens || []; |
||||
} |
||||
|
||||
export function getToken(tokenCaip19Id: TokenCaip19Id) { |
||||
return getTokens().find((t) => t.tokenCaip19Id === tokenCaip19Id); |
||||
} |
||||
|
||||
export function findTokensByAddress(address: Address) { |
||||
return getTokens().filter((t) => t.tokenCaip19Id.includes(address)); |
||||
} |
||||
|
||||
export function isIbcToken(token: TokenMetadata) { |
||||
return Object.values(IbcTokenTypes).includes(token.type as IbcTokenTypes); |
||||
} |
@ -1,125 +0,0 @@ |
||||
import { z } from 'zod'; |
||||
|
||||
import { ERC20Metadata, TokenType } from '@hyperlane-xyz/sdk'; |
||||
import { ProtocolType } from '@hyperlane-xyz/utils'; |
||||
|
||||
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply'>; |
||||
|
||||
// Extend SDK's TokenType enum to allow for IBC token routes
|
||||
export enum IbcTokenTypes { |
||||
IbcNative = 'ibc-native', |
||||
} |
||||
type ExtendedTokenType = TokenType | IbcTokenTypes; |
||||
|
||||
// Define common fields for all token types
|
||||
const commonTokenFields = z.object({ |
||||
chainId: z.union([z.number().positive(), z.string()]), |
||||
name: z.string().optional(), |
||||
symbol: z.string().optional(), |
||||
decimals: z.number().nonnegative().optional(), // decimals == 0 for NFTs
|
||||
logoURI: z.string().optional(), |
||||
igpTokenAddressOrDenom: z.string().optional(), |
||||
}); |
||||
type CommonTokenFields = z.infer<typeof commonTokenFields>; |
||||
|
||||
/** |
||||
* Types for the developer-provided config |
||||
* Seems redundant with the *Metadata types below but these are |
||||
* necessary to enable a more flexible and intuitive schema for the config |
||||
* E.g. allow literal strings for 'type' field |
||||
* or allow omitting 'address' for NativeTokenConfig |
||||
* |
||||
* See src/consts/tokens.ts |
||||
*/ |
||||
|
||||
type CommonFieldsWithLooseProtocol = Omit<CommonTokenFields, 'protocol'> & { |
||||
protocol?: `${ProtocolType}`; |
||||
}; |
||||
interface BaseTokenConfig extends CommonFieldsWithLooseProtocol { |
||||
type: `${ExtendedTokenType}`; // use template literal to allow string values
|
||||
} |
||||
|
||||
const CollateralTokenSchema = commonTokenFields.extend({ |
||||
type: z.literal(TokenType.collateral), |
||||
address: z.string(), |
||||
hypCollateralAddress: z.string(), |
||||
isNft: z.boolean().optional(), |
||||
isSpl2022: z.boolean().optional(), // Only required if using a 2022 version SPL Token on a Sealevel chain
|
||||
}); |
||||
|
||||
interface CollateralTokenConfig extends BaseTokenConfig { |
||||
// Typescript does not allow literal value value 'collateral' even if it matches the enum's value
|
||||
type: TokenType.collateral | 'collateral'; |
||||
address: Address; |
||||
hypCollateralAddress: Address; |
||||
isNft?: boolean; |
||||
isSpl2022?: boolean; |
||||
} |
||||
|
||||
const NativeTokenSchema = commonTokenFields.extend({ |
||||
type: z.literal(TokenType.native), |
||||
hypNativeAddress: z.string(), |
||||
}); |
||||
|
||||
interface NativeTokenConfig extends BaseTokenConfig { |
||||
type: TokenType.native | 'native'; |
||||
hypNativeAddress: Address; |
||||
} |
||||
|
||||
const IbcNativeTokenSchema = commonTokenFields.extend({ |
||||
type: z.literal(IbcTokenTypes.IbcNative), |
||||
}); |
||||
|
||||
interface IbcNativeTokenConfig extends BaseTokenConfig { |
||||
type: IbcTokenTypes.IbcNative | 'ibc-native'; |
||||
} |
||||
|
||||
export const WarpTokenConfigSchema = z.array( |
||||
CollateralTokenSchema.or(NativeTokenSchema).or(IbcNativeTokenSchema), |
||||
); |
||||
export type WarpTokenConfig = Array< |
||||
CollateralTokenConfig | NativeTokenConfig | IbcNativeTokenConfig |
||||
>; |
||||
|
||||
/** |
||||
* Types for use in the app after processing config |
||||
* Uses unambiguous CAIP IDs |
||||
* |
||||
* See src/features/tokens/metadata.ts |
||||
*/ |
||||
interface BaseTokenMetadata extends MinimalTokenMetadata { |
||||
type: ExtendedTokenType; |
||||
tokenCaip19Id: TokenCaip19Id; |
||||
routerAddress: Address; // Shared name for hypCollateralAddr or hypNativeAddr
|
||||
igpTokenAddressOrDenom?: Address; |
||||
logoURI?: string; |
||||
} |
||||
|
||||
interface CollateralTokenMetadata extends BaseTokenMetadata { |
||||
type: TokenType.collateral; |
||||
} |
||||
|
||||
interface NativeTokenMetadata extends BaseTokenMetadata { |
||||
type: TokenType.native; |
||||
} |
||||
|
||||
interface IbcNativeTokenMetadata extends BaseTokenMetadata { |
||||
type: IbcTokenTypes.IbcNative; |
||||
} |
||||
|
||||
export type TokenMetadata = CollateralTokenMetadata | NativeTokenMetadata | IbcNativeTokenMetadata; |
||||
|
||||
/** |
||||
* Extended types including synthetic hyp token addresses |
||||
*/ |
||||
interface HypTokens { |
||||
hypTokens: Array<{ chain: ChainCaip2Id; router: Address; decimals: number }>; |
||||
} |
||||
|
||||
type NativeTokenMetadataWithHypTokens = NativeTokenMetadata & HypTokens; |
||||
type CollateralTokenMetadataWithHypTokens = CollateralTokenMetadata & HypTokens; |
||||
type IbcNativeTokenMetadataWithHypTokens = IbcNativeTokenMetadata & HypTokens; |
||||
export type TokenMetadataWithHypTokens = |
||||
| CollateralTokenMetadataWithHypTokens |
||||
| NativeTokenMetadataWithHypTokens |
||||
| IbcNativeTokenMetadataWithHypTokens; |
@ -0,0 +1,45 @@ |
||||
import { useMutation } from '@tanstack/react-query'; |
||||
|
||||
import { TokenAmount } from '@hyperlane-xyz/sdk'; |
||||
import { ProtocolType, timeout } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { getWarpCore } from '../../context/context'; |
||||
import { logger } from '../../utils/logger'; |
||||
import { getAccountAddressAndPubKey } from '../wallet/hooks/multiProtocol'; |
||||
import { AccountInfo } from '../wallet/hooks/types'; |
||||
|
||||
const MAX_FETCH_TIMEOUT = 3000; // 3 seconds
|
||||
|
||||
interface FetchMaxParams { |
||||
accounts: Record<ProtocolType, AccountInfo>; |
||||
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; |
||||
} |
||||
} |
@ -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]); |
||||
} |
@ -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<HexString>, |
||||
): 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, |
||||
}); |
||||
} |
@ -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<IgpQuote> { |
||||
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, |
||||
}, |
||||
}; |
||||
} |
@ -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<Record<keyof TransferFormValues, string>>; |
||||
type Balances = AppState['balances']; |
||||
|
||||
export async function validateFormValues( |
||||
values: TransferFormValues, |
||||
tokenRoutes: RoutesMap, |
||||
balances: Balances, |
||||
igpQuote: IgpQuote | null, |
||||
accounts: Record<ProtocolType, AccountInfo>, |
||||
): Promise<FormError> { |
||||
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<ProtocolType, AccountInfo>; |
||||
}): Promise<FormError | null> { |
||||
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<ProtocolType, AccountInfo>, |
||||
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; |
||||
} |
||||
} |
@ -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 |
@ -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)); |
@ -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, |
||||
}, |
||||
], |
||||
}, |
||||
}); |
||||
}); |
||||
}); |
@ -1,212 +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<RoutesMap> { |
||||
logger.info('Searching for token routes'); |
||||
const processedTokens: TokenMetadataWithHypTokens[] = []; |
||||
for (const token of context.tokens) { |
||||
// Skip querying of IBC tokens
|
||||
if (isIbcToken(token)) continue; |
||||
const tokenWithHypTokens = await fetchRemoteHypTokens(context, token); |
||||
processedTokens.push(tokenWithHypTokens); |
||||
} |
||||
let routes = computeTokenRoutes(processedTokens); |
||||
|
||||
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<TokenMetadataWithHypTokens> { |
||||
const { |
||||
symbol: baseSymbol, |
||||
tokenCaip19Id: baseTokenCaip19Id, |
||||
routerAddress: baseRouter, |
||||
} = baseToken; |
||||
const isNft = isNonFungibleToken(baseTokenCaip19Id); |
||||
logger.info(`Fetching remote tokens for symbol ${baseSymbol} (${baseTokenCaip19Id})`); |
||||
|
||||
const baseAdapter = AdapterFactory.HypCollateralAdapterFromAddress(baseTokenCaip19Id, baseRouter); |
||||
|
||||
const remoteRouters = await baseAdapter.getAllRouters(); |
||||
logger.info(`Router addresses found:`, remoteRouters.length); |
||||
|
||||
const hypTokens = await Promise.all( |
||||
remoteRouters.map(async (router) => { |
||||
const destMetadata = context.multiProvider.getChainMetadata(router.domain); |
||||
const protocol = destMetadata.protocol || ProtocolType.Ethereum; |
||||
const chain = getCaip2Id(protocol, context.multiProvider.getChainId(router.domain)); |
||||
const formattedAddress = bytesToProtocolAddress(router.address, protocol); |
||||
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<ChainCaip2Id>(); |
||||
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; |
||||
} |
@ -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<TokenMetadata[]> { |
||||
const result = WarpTokenConfigSchema.safeParse(configList); |
||||
if (!result.success) { |
||||
logger.warn('Invalid token config', result.error); |
||||
throw new Error(`Invalid token config: ${result.error.toString()}`); |
||||
} |
||||
|
||||
const parsedConfig = result.data; |
||||
const tokenMetadata: TokenMetadata[] = []; |
||||
for (const config of parsedConfig) { |
||||
const { type, chainId, logoURI, 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<MinimalTokenMetadata> { |
||||
const { type, chainId, name, symbol, decimals } = tokenConfig; |
||||
if (name && symbol && decimals) { |
||||
// Already provided in the config
|
||||
return { name, symbol, decimals }; |
||||
} |
||||
|
||||
const chainMetadata = multiProvider.getChainMetadata(chainId); |
||||
|
||||
if (type === TokenType.native) { |
||||
// Use the native token config that may be in the chain metadata
|
||||
const tokenMetadata = chainMetadata.nativeToken; |
||||
if (!tokenMetadata) throw new Error('Name, symbol, or decimals is missing for native token'); |
||||
return tokenMetadata; |
||||
} |
||||
|
||||
if (type === TokenType.collateral) { |
||||
// Fetch the data from the contract
|
||||
let tokenAdapter: ITokenAdapter; |
||||
if (protocol === ProtocolType.Ethereum) { |
||||
const provider = multiProvider.getEthersV5Provider(chainId); |
||||
const collateralContract = getHypErc20CollateralContract(routerAddress, provider); |
||||
const wrappedTokenAddr = await collateralContract.wrappedToken(); |
||||
tokenAdapter = new EvmTokenAdapter(chainMetadata.name, multiProvider, { |
||||
token: wrappedTokenAddr, |
||||
}); |
||||
} else { |
||||
// TODO solana support when hyp tokens have metadata
|
||||
throw new Error('Name, symbol, and decimals is required for non-EVM token configs'); |
||||
} |
||||
return tokenAdapter.getMetadata(isNft); |
||||
} |
||||
|
||||
throw new Error(`Unsupported token type ${type}`); |
||||
} |
@ -1,6 +0,0 @@ |
||||
import fs from 'fs'; |
||||
import { parse } from 'yaml'; |
||||
|
||||
export function readYaml(path: string) { |
||||
return parse(fs.readFileSync(path, 'utf-8')); |
||||
} |
@ -0,0 +1,15 @@ |
||||
import { SafeParseReturnType } from 'zod'; |
||||
|
||||
import { logger } from './logger'; |
||||
|
||||
export function validateZodResult<T>( |
||||
result: SafeParseReturnType<T, T>, |
||||
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; |
||||
} |
||||
} |
Loading…
Reference in new issue