diff --git a/apps/kyberswap-interface/src/pages/CrossChainSwap/hooks/useCrossChainSwap.tsx b/apps/kyberswap-interface/src/pages/CrossChainSwap/hooks/useCrossChainSwap.tsx index f2e3f5e720..a5f3b35680 100644 --- a/apps/kyberswap-interface/src/pages/CrossChainSwap/hooks/useCrossChainSwap.tsx +++ b/apps/kyberswap-interface/src/pages/CrossChainSwap/hooks/useCrossChainSwap.tsx @@ -1,4 +1,4 @@ -import { ChainId } from '@kyberswap/ks-sdk-core' +import { ChainId, Currency as EvmCurrency } from '@kyberswap/ks-sdk-core' import { t } from '@lingui/macro' import { useWalletSelector } from '@near-wallet-selector/react-hook' import { adaptSolanaWallet } from '@reservoir0x/relay-solana-wallet-adapter' @@ -34,7 +34,7 @@ import { } from 'pages/CrossChainSwap/adapters' import { CrossChainSwapFactory } from 'pages/CrossChainSwap/factory' import { type NearToken, useNearTokens } from 'pages/CrossChainSwap/hooks/useNearTokens' -import { useSolanaTokens } from 'pages/CrossChainSwap/hooks/useSolanaTokens' +import { type SolanaToken, useSolanaTokens } from 'pages/CrossChainSwap/hooks/useSolanaTokens' import { CrossChainSwapAdapterRegistry, Quote } from 'pages/CrossChainSwap/registry' import { NEAR_STABLE_COINS, SOLANA_STABLE_COINS, isCanonicalPair } from 'pages/CrossChainSwap/utils' import { useAppSelector } from 'state/hooks' @@ -53,6 +53,78 @@ const SSE_EVENT = { // Timeout constants const SOFT_TIMEOUT_MS = 4000 // 4 seconds - enable swap button if at least 1 quote exists +type PairCategory = 'stablePair' | 'commonPair' | 'highVolatilityPair' | 'exoticPair' + +type EvmCrossChainCurrency = EvmCurrency & { + chainId: ChainId + isNative?: boolean + wrapped: { + address: string + decimals: number + isStable?: boolean + } +} + +type TokenPrice = { + PriceBuy?: number + PriceSell?: number +} + +type TokenPricesResponse = { + data?: Record | undefined> +} + +type TokenCategoryResponse = { + data?: Array<{ + token: string + category?: PairCategory + }> +} + +const isObject = (value: unknown): value is Record => typeof value === 'object' && value !== null + +const isEvmCurrency = (currency: Currency | undefined): currency is EvmCrossChainCurrency => { + const value: unknown = currency + if (!isObject(value)) return false + + const wrapped = value.wrapped + if (!isObject(wrapped)) return false + + return typeof wrapped.address === 'string' && typeof wrapped.decimals === 'number' +} + +const isSolanaToken = (currency: Currency | undefined): currency is SolanaToken => { + const value: unknown = currency + return isObject(value) && typeof value.id === 'string' +} + +const isNearToken = (currency: Currency | undefined): currency is NearToken => { + const value: unknown = currency + return isObject(value) && typeof value.assetId === 'string' +} + +const getTokenMidPriceUsd = (price: TokenPrice | undefined) => { + const buy = price?.PriceBuy + const sell = price?.PriceSell + + if (typeof buy === 'number' && typeof sell === 'number') return (buy + sell) / 2 + return buy || sell || 0 +} + +const getCurrencyAddress = (currency: Currency) => { + if (isEvmCurrency(currency)) return currency.isNative ? ZERO_ADDRESS : currency.wrapped.address + if (isSolanaToken(currency)) return currency.id + if (isNearToken(currency)) return currency.assetId + return currency.symbol || '' +} + +const isStableCurrency = (currency: Currency | undefined, chain: Chain | undefined) => { + if (isEvmCurrency(currency)) return !!currency.wrapped.isStable + if (chain === NonEvmChain.Solana && isSolanaToken(currency)) return SOLANA_STABLE_COINS.includes(currency.id) + if (chain === NonEvmChain.Near && isNearToken(currency)) return NEAR_STABLE_COINS.includes(currency.assetId) + return false +} + const getDefaultTokenForChain = (chain: string | null | undefined) => { if (chain === NonEvmChain.Near) return 'near' if (chain === NonEvmChain.Solana) return SOLANA_NATIVE @@ -310,7 +382,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R currencyIn ? parseUnits( amountDebounce || '0', - isFromEvm ? (currencyIn as any).wrapped.decimals : currencyIn.decimals, + isFromEvm && isEvmCurrency(currencyIn) ? currencyIn.wrapped.decimals : currencyIn.decimals, ).toString() : undefined, [currencyIn, amountDebounce, isFromEvm], @@ -334,9 +406,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R setSelectedAdapter(null) }, [currencyIn, currencyOut, fromChainId, toChainId]) - const [category, setCategory] = useState<'stablePair' | 'commonPair' | 'highVolatilityPair' | 'exoticPair'>( - 'commonPair', - ) + const [category, setCategory] = useState('commonPair') const warning = useMemo(() => { const highSlippageMsg = t`Your slippage is set higher than usual, which may cause unexpected losses` const lowSlippageMsg = t`Your slippage is set lower than usual, which may cause transaction failure.` @@ -432,22 +502,16 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R const { signal } = abortControllerRef.current const body: Record = {} - if ((currencyIn as any)?.wrapped?.address) { + if (isEvmCurrency(currencyIn)) { if (!body[fromChainId]) body[fromChainId] = [] - body[fromChainId].push((currencyIn as any)?.wrapped?.address) + body[fromChainId].push(currencyIn.wrapped.address) } - if ((currencyOut as any)?.wrapped?.address) { + if (isEvmCurrency(currencyOut)) { if (!body[toChainId]) body[toChainId] = [] - body[toChainId].push((currencyOut as any)?.wrapped?.address) + body[toChainId].push(currencyOut.wrapped.address) } - const r: { - data: { - [chainId: string]: { - [address: string]: { PriceBuy: number; PriceSell: number } - } - } - } = await fetch(`${TOKEN_API_URL}/v1/public/tokens/prices`, { + const r: TokenPricesResponse = await fetch(`${TOKEN_API_URL}/v1/public/tokens/prices`, { method: 'POST', body: JSON.stringify(body), signal, @@ -455,8 +519,12 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R // Check if this request has been aborted if (signal.aborted) return - const tokenInUsd = r?.data?.[fromChainId]?.[(currencyIn as any).wrapped.address]?.PriceBuy || 0 - const tokenOutUsd = r?.data?.[toChainId as any]?.[(currencyOut as any).wrapped.address]?.PriceBuy || 0 + const tokenInUsd = isEvmCurrency(currencyIn) + ? getTokenMidPriceUsd(r.data?.[fromChainId]?.[currencyIn.wrapped.address]) + : 0 + const tokenOutUsd = isEvmCurrency(currencyOut) + ? getTokenMidPriceUsd(r.data?.[toChainId]?.[currencyOut.wrapped.address]) + : 0 const isToNear = toChainId === 'near' let feeBps = 25 @@ -464,40 +532,36 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R feeBps = 25 } else if (isFromEvm && isToEvm) { if ( + isEvmCurrency(currencyIn) && + isEvmCurrency(currencyOut) && isCanonicalPair( - (currencyIn as any).chainId, - (currencyIn as any).wrapped.address, - (currencyOut as any).chainId, - (currencyOut as any).wrapped.address, + currencyIn.chainId, + currencyIn.wrapped.address, + currencyOut.chainId, + currencyOut.wrapped.address, ) ) { setCategory('stablePair') feeBps = 5 } else { + const currencyInAddress = isEvmCurrency(currencyIn) ? currencyIn.wrapped.address : '' + const currencyOutAddress = isEvmCurrency(currencyOut) ? currencyOut.wrapped.address : '' const [token0Cat, token1Cat] = await Promise.all([ await fetch( - `${TOKEN_API_URL}/v1/public/category/token?tokens=${( - currencyIn as any - ).wrapped.address.toLowerCase()}&chainId=${fromChainId}`, + `${TOKEN_API_URL}/v1/public/category/token?tokens=${currencyInAddress.toLowerCase()}&chainId=${fromChainId}`, ) .then(res => res.json()) - .then(res => { - const cat = res?.data?.find( - (item: any) => item.token.toLowerCase() === (currencyIn as any).wrapped.address.toLowerCase(), - ) + .then((res: TokenCategoryResponse) => { + const cat = res?.data?.find(item => item.token.toLowerCase() === currencyInAddress.toLowerCase()) return cat?.category || 'exoticPair' }), await fetch( - `${TOKEN_API_URL}/v1/public/category/token?tokens=${( - currencyOut as any - ).wrapped.address.toLowerCase()}&chainId=${toChainId}`, + `${TOKEN_API_URL}/v1/public/category/token?tokens=${currencyOutAddress.toLowerCase()}&chainId=${toChainId}`, ) .then(res => res.json()) - .then(res => { - const cat = res?.data?.find( - (item: any) => item.token.toLowerCase() === (currencyOut as any).wrapped.address.toLowerCase(), - ) + .then((res: TokenCategoryResponse) => { + const cat = res?.data?.find(item => item.token.toLowerCase() === currencyOutAddress.toLowerCase()) return cat?.category || 'exoticPair' }), ]) @@ -511,7 +575,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R feeBps = 15 } else if ( (token0Cat === 'stablePair' && token1Cat === 'stablePair') || - ((currencyIn as any)?.wrapped?.isStable && (currencyOut as any)?.wrapped?.isStable) + (isStableCurrency(currencyIn, fromChainId) && isStableCurrency(currencyOut, toChainId)) ) { setCategory('stablePair') feeBps = 5 @@ -522,20 +586,8 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R } } } else if (isFromNear || isToNear || isFromSolana || isToSolana) { - const isTokenInStable = isFromEvm - ? (currencyIn as any)?.wrapped?.isStable - : isFromSolana - ? SOLANA_STABLE_COINS.includes((currencyIn as any).id) - : isFromNear - ? NEAR_STABLE_COINS.includes((currencyIn as any).assetId) - : false - const isTokenOutStable = isToEvm - ? (currencyOut as any)?.wrapped?.isStable - : isToSolana - ? SOLANA_STABLE_COINS.includes((currencyOut as any).id) - : isToNear - ? NEAR_STABLE_COINS.includes((currencyOut as any).assetId) - : false + const isTokenInStable = isStableCurrency(currencyIn, fromChainId) + const isTokenOutStable = isStableCurrency(currencyOut, toChainId) if (!isFromEvm && !isToEvm) { feeBps = 25 @@ -604,18 +656,8 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R // The streaming API handles provider selection on the backend // Construct the API URL with parameters - const fromToken = (params.fromToken as any).isNative - ? ZERO_ADDRESS - : (params.fromToken as any).wrapped?.address || - (params.fromToken as any).address || - (params.fromToken as any).id || - (params.fromToken as any).assetId - const toToken = (params.toToken as any).isNative - ? ZERO_ADDRESS - : (params.toToken as any).wrapped?.address || - (params.toToken as any).address || - (params.toToken as any).id || - (params.toToken as any).assetId + const fromToken = getCurrencyAddress(params.fromToken) + const toToken = getCurrencyAddress(params.toToken) const fromTokenDecimals = params.fromToken.decimals const toTokenDecimals = params.toToken.decimals @@ -633,8 +675,8 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R integrator: 'kyberswap', stream: 'true', slippage: params.slippage.toString(), - fromTokenUsd: (params as any).tokenInUsd?.toString() || '0', - toTokenUsd: (params as any).tokenOutUsd?.toString() || '0', + fromTokenUsd: params.tokenInUsd?.toString() || '0', + toTokenUsd: params.tokenOutUsd?.toString() || '0', }) // Add includedSources and excludedSources parameters @@ -932,11 +974,12 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R } } + const sendSolanaTransaction = connection.sendTransaction as unknown as Parameters[3] const adaptedWallet = adaptSolanaWallet( solanaAddress?.toString() || CROSS_CHAIN_FEE_RECEIVER_SOLANA, 792703809, //chain id that Relay uses to identify solana connection, - connection.sendTransaction as any, + sendSolanaTransaction, ) try { @@ -950,7 +993,7 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R toToken: currencyOut, amount: inputAmount, slippage, - walletClient: (fromChainId === 'solana' ? adaptedWallet : walletClient?.data) as any, + walletClient: fromChainId === 'solana' ? adaptedWallet : walletClient?.data, sender, recipient: receiver, nearTokens,