diff --git a/.changeset/seven-tigers-check.md b/.changeset/seven-tigers-check.md new file mode 100644 index 000000000..2e239ece7 --- /dev/null +++ b/.changeset/seven-tigers-check.md @@ -0,0 +1,5 @@ +--- +'@reservoir0x/relay-sdk': patch +--- + +Add EOA detection diff --git a/packages/sdk/src/actions/getQuote.ts b/packages/sdk/src/actions/getQuote.ts index 6745e5a0e..0285522ca 100644 --- a/packages/sdk/src/actions/getQuote.ts +++ b/packages/sdk/src/actions/getQuote.ts @@ -137,6 +137,7 @@ export async function getQuote( throw new Error('Recipient is required') } + const query: QuoteBody = { user: includeDefaultParameters ? (defaultParameters?.user as string) diff --git a/packages/sdk/src/types/AdaptedWallet.ts b/packages/sdk/src/types/AdaptedWallet.ts index 4b638f55c..aeb535ccb 100644 --- a/packages/sdk/src/types/AdaptedWallet.ts +++ b/packages/sdk/src/types/AdaptedWallet.ts @@ -51,4 +51,6 @@ export type AdaptedWallet = { chainId: number, items: TransactionStepItem[] ) => Promise + // detect if wallet is an EOA (externally owned account) + isEOA?: (chainId: number) => Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }> } diff --git a/packages/sdk/src/utils/viemWallet.ts b/packages/sdk/src/utils/viemWallet.ts index 5f0fcde23..adbe4d516 100644 --- a/packages/sdk/src/utils/viemWallet.ts +++ b/packages/sdk/src/utils/viemWallet.ts @@ -242,6 +242,64 @@ export const adaptViemWallet = (wallet: WalletClient): AdaptedWallet => { }) return id + }, + isEOA: async ( + chainId: number + ): Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }> => { + if (!wallet.account) { + return { isEOA: false, isEIP7702Delegated: false } + } + + try { + let hasSmartWalletCapabilities = false + try { + const capabilities = await wallet.getCapabilities({ + account: wallet.account, + chainId + }) + + hasSmartWalletCapabilities = Boolean( + capabilities?.atomicBatch?.supported || + capabilities?.paymasterService?.supported || + capabilities?.auxiliaryFunds?.supported || + capabilities?.sessionKeys?.supported + ) + } catch (capabilitiesError) {} + + const client = getClient() + const chain = client.chains.find((chain) => chain.id === chainId) + const rpcUrl = chain?.httpRpcUrl + + if (!chain) { + throw new Error(`Chain ${chainId} not found in relay client`) + } + + const viemClient = createPublicClient({ + chain: chain?.viemChain, + transport: rpcUrl ? http(rpcUrl) : http() + }) + + let code + try { + code = await viemClient.getCode({ + address: wallet.account.address + }) + } catch (getCodeError) { + throw getCodeError + } + + const hasCode = Boolean(code && code !== '0x') + const isEIP7702Delegated = Boolean( + code && code.toLowerCase().startsWith('0xef01') + ) + const isSmartWallet = + hasSmartWalletCapabilities || hasCode || isEIP7702Delegated + const isEOA = !isSmartWallet + + return { isEOA, isEIP7702Delegated } + } catch (error) { + return { isEOA: false, isEIP7702Delegated: false } + } } } } diff --git a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx index 3a9405261..da5aa0d5f 100644 --- a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx +++ b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx @@ -1,10 +1,4 @@ -import type { - ComponentPropsWithoutRef, - Dispatch, - FC, - ReactNode, - SetStateAction -} from 'react' +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCurrencyBalance, @@ -16,7 +10,8 @@ import { usePreviousValueChange, useIsWalletCompatible, useFallbackState, - useGasTopUpRequired + useGasTopUpRequired, + useEOADetection } from '../../hooks/index.js' import type { Address, WalletClient } from 'viem' import { formatUnits, parseUnits } from 'viem' @@ -565,12 +560,19 @@ const SwapWidgetRenderer: FC = ({ isLoadingFromTokenPrice, debouncedInputAmountValue, tradeType, - originChainSupportsProtocolv2 + originChainSupportsProtocolv2, + fromChain?.id ]) const loadingProtocolVersion = fromChain?.id && originChainSupportsProtocolv2 && isLoadingFromTokenPrice + const explicitDeposit = useEOADetection( + wallet, + quoteProtocol, + fromToken?.chainId, + fromChain?.vmType + ) const normalizedSponsoredTokens = useMemo(() => { const chainVms = relayClient?.chains.reduce( (chains, chain) => { @@ -605,8 +607,15 @@ const SwapWidgetRenderer: FC = ({ normalizedSponsoredTokens.includes(normalizedToToken) && normalizedSponsoredTokens.includes(normalizedFromToken) + const shouldSetQuoteParameters = + fromToken && + toToken && + (quoteProtocol !== 'preferV2' || + fromChain?.vmType !== 'evm' || + explicitDeposit !== undefined) + const quoteParameters: Parameters['2'] = - fromToken && toToken + shouldSetQuoteParameters ? { user: fromAddressWithFallback, originChainId: fromToken.chainId, @@ -633,7 +642,11 @@ const SwapWidgetRenderer: FC = ({ refundTo: fromToken?.chainId === 1337 ? address : undefined, slippageTolerance: slippageTolerance, topupGas: gasTopUpEnabled && gasTopUpRequired, - protocolVersion: quoteProtocol + protocolVersion: quoteProtocol, + ...(quoteProtocol === 'preferV2' && + explicitDeposit !== undefined && { + explicitDeposit: explicitDeposit + }) } : undefined @@ -711,7 +724,7 @@ const SwapWidgetRenderer: FC = ({ onQuoteReceived, { refetchOnWindowFocus: false, - enabled: quoteFetchingEnabled, + enabled: quoteFetchingEnabled && quoteParameters !== undefined, refetchInterval: !transactionModalOpen && !depositAddressModalOpen && diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts index 025dcedc6..a085a14b2 100644 --- a/packages/ui/src/hooks/index.ts +++ b/packages/ui/src/hooks/index.ts @@ -21,6 +21,7 @@ import useMoonPayTransaction from './useMoonPayTransaction.js' import { useInternalRelayChains } from './useInternalRelayChains.js' import useGasTopUpRequired from './useGasTopUpRequired.js' import useHyperliquidUsdcBalance from './useHyperliquidUsdcBalance.js' +import useEOADetection from './useEOADetection.js' export { useMounted, @@ -45,5 +46,6 @@ export { useMoonPayTransaction, useInternalRelayChains, useGasTopUpRequired, - useHyperliquidUsdcBalance + useHyperliquidUsdcBalance, + useEOADetection } diff --git a/packages/ui/src/hooks/useEOADetection.ts b/packages/ui/src/hooks/useEOADetection.ts new file mode 100644 index 000000000..e872cdccc --- /dev/null +++ b/packages/ui/src/hooks/useEOADetection.ts @@ -0,0 +1,88 @@ +import { useMemo, useEffect, useState, useRef } from 'react' +import type { AdaptedWallet } from '@relayprotocol/relay-sdk' + +/** + * Hook to detect if a wallet is an EOA and return the appropriate explicitDeposit flag + * Only runs detection when protocol version is 'preferV2' and wallet supports EOA detection + */ +const useEOADetection = ( + wallet?: AdaptedWallet, + protocolVersion?: string, + chainId?: number, + chainVmType?: string +): boolean | undefined => { + const [detectionState, setDetectionState] = useState<{ + value: boolean | undefined + conditionKey: string + }>({ value: undefined, conditionKey: '' }) + + const walletRef = useRef(wallet) + const walletId = useRef(0) + + if (walletRef.current !== wallet) { + walletRef.current = wallet + walletId.current += 1 + } + + const conditionKey = `${wallet?.vmType}:${chainVmType}:${!!wallet?.isEOA}:${protocolVersion}:${chainId}:${walletId.current}` + + const shouldDetect = useMemo(() => { + return ( + wallet !== undefined && + protocolVersion === 'preferV2' && + chainId !== undefined && + wallet?.vmType === 'evm' && + chainVmType === 'evm' + ) + }, [wallet?.vmType, protocolVersion, chainId, chainVmType]) + + // Synchronously return undefined when conditions change + const explicitDeposit = useMemo(() => { + return detectionState.conditionKey !== conditionKey || !shouldDetect + ? undefined + : detectionState.value + }, [conditionKey, shouldDetect, detectionState]) + + useEffect(() => { + setDetectionState({ value: undefined, conditionKey }) + + if (!shouldDetect) { + return + } + + const detectEOA = async () => { + try { + if (!wallet?.isEOA) { + setDetectionState((current) => + current.conditionKey === conditionKey + ? { value: false, conditionKey } + : current + ) + return + } + + const eoaResult = await wallet.isEOA(chainId!) + const { isEOA, isEIP7702Delegated } = eoaResult + const explicitDepositValue = !isEOA || isEIP7702Delegated + + setDetectionState((current) => + current.conditionKey === conditionKey + ? { value: explicitDepositValue, conditionKey } + : current + ) + } catch (error) { + setDetectionState((current) => + current.conditionKey === conditionKey + ? { value: undefined, conditionKey } + : current + ) + } + } + + detectEOA() + }, [conditionKey, shouldDetect, wallet, chainId]) + + return explicitDeposit +} + +export default useEOADetection