Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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<string, Record<string, TokenPrice | undefined> | undefined>
}

type TokenCategoryResponse = {
data?: Array<{
token: string
category?: PairCategory
}>
}

const isObject = (value: unknown): value is Record<string, unknown> => 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
Expand Down Expand Up @@ -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],
Expand All @@ -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<PairCategory>('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.`
Expand Down Expand Up @@ -432,72 +502,66 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
const { signal } = abortControllerRef.current

const body: Record<string, string[]> = {}
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,
}).then(r => r.json())
// 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
if (isFromBitcoin || isToBitcoin) {
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'
}),
])
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -932,11 +974,12 @@ export const CrossChainSwapRegistryProvider = ({ children }: { children: React.R
}
}

const sendSolanaTransaction = connection.sendTransaction as unknown as Parameters<typeof adaptSolanaWallet>[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 {
Expand All @@ -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,
Expand Down
Loading