Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions .changeset/witty-clocks-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@relayprotocol/relay-sdk': minor
'@relayprotocol/relay-kit-ui': minor
---

Refactor EOA detection to improve ux
8 changes: 5 additions & 3 deletions packages/sdk/src/types/AdaptedWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export type AdaptedWallet = {
items: TransactionStepItem[]
) => Promise<string | undefined>
// detect if wallet is an EOA (externally owned account)
isEOA?: (
chainId: number
) => Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }>
// Note: returns isEOA: false when explicit deposit should be used
isEOA?: (chainId: number) => Promise<{
isEOA: boolean
isEIP7702Delegated: boolean
}>
}
2 changes: 1 addition & 1 deletion packages/sdk/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { request, APIError, isAPIError } from './request.js'
export { log, LogLevel } from './logger.js'
export { axios } from './axios.js'
export { default as prepareCallTransaction } from './prepareCallTransaction.js'
export { adaptViemWallet } from './viemWallet.js'
export { adaptViemWallet, isViemWalletClient } from './viemWallet.js'
export { convertViemChainToRelayChain, type RelayAPIChain } from './chain.js'
export { getCurrentStepData } from './getCurrentStepData.js'
export {
Expand Down
247 changes: 210 additions & 37 deletions packages/sdk/src/utils/viemWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ import {
http
} from 'viem'

// Cache for expensive RPC calls (code, balance, tx count)
interface EOACacheEntry {
code?: { value: string | undefined; timestamp: number }
balance?: { value: bigint; timestamp: number }
txCount?: { value: number; timestamp: number }
}

const CACHE_DURATION_MS = 2 * 60 * 1000 // 2 minutes
const eoaCache = new Map<string, EOACacheEntry>()

function getCacheKey(address: string, chainId: number): string {
return `${address}-${chainId}`
}

export function isViemWalletClient(
wallet: WalletClient | AdaptedWallet
): wallet is WalletClient {
Expand Down Expand Up @@ -245,60 +259,219 @@ export const adaptViemWallet = (wallet: WalletClient): AdaptedWallet => {
},
isEOA: async (
chainId: number
): Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }> => {
): Promise<{
isEOA: boolean
isEIP7702Delegated: boolean
}> => {
if (!wallet.account) {
return { isEOA: false, isEIP7702Delegated: false }
return {
isEOA: false,
isEIP7702Delegated: false
}
}

try {
let hasSmartWalletCapabilities = false
try {
const capabilities = await wallet.getCapabilities({
account: wallet.account,
chainId
const address = wallet.account.address
const cacheKey = getCacheKey(address, chainId)
const now = Date.now()

// Get or create cache entry for this address+chain
let cacheEntry = eoaCache.get(cacheKey)
if (!cacheEntry) {
cacheEntry = {}
eoaCache.set(cacheKey, cacheEntry)
}

// Always fetch capabilities fresh (wallet-specific, not cacheable)
const getSmartWalletCapabilities = async () => {
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) {}
return _hasSmartWalletCapabilities
}

// Get code with caching
const getCode = async () => {
// Check cache first
console.log('CHECKING CODE CACHE')
if (
cacheEntry &&
cacheEntry?.code &&
now - cacheEntry?.code.timestamp < CACHE_DURATION_MS
) {
console.log('CODE CACHE HIT')
const code = cacheEntry!.code.value
const hasCode = Boolean(code && code !== '0x')
const isEIP7702Delegated = Boolean(
code && code.toLowerCase().startsWith('0xef01')
)
return { hasCode, isEIP7702Delegated }
}

// Fetch from RPC
console.log('FETCHING CODE FROM RPC')
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()
})

hasSmartWalletCapabilities = Boolean(
capabilities?.atomicBatch?.supported ||
capabilities?.paymasterService?.supported ||
capabilities?.auxiliaryFunds?.supported ||
capabilities?.sessionKeys?.supported
)
} catch (capabilitiesError) {}
try {
const _code = await viemClient.getCode({ address })

const client = getClient()
const chain = client.chains.find((chain) => chain.id === chainId)
const rpcUrl = chain?.httpRpcUrl
// Cache the result
cacheEntry!.code = { value: _code, timestamp: now }

if (!chain) {
throw new Error(`Chain ${chainId} not found in relay client`)
const hasCode = Boolean(_code && _code !== '0x')
const isEIP7702Delegated = Boolean(
_code && _code.toLowerCase().startsWith('0xef01')
)
return { hasCode, isEIP7702Delegated }
} catch (getCodeError) {
throw getCodeError
}
}

const viemClient = createPublicClient({
chain: chain?.viemChain,
transport: rpcUrl ? http(rpcUrl) : http()
})
// Get balance with caching
const getNativeBalance = async () => {
// Check cache first
console.log('CHECKING BALANCE CACHE')
if (
cacheEntry &&
cacheEntry?.balance &&
now - cacheEntry?.balance.timestamp < CACHE_DURATION_MS
) {
console.log('BALANCE CACHE HIT')
return cacheEntry?.balance.value
}

let code
try {
code = await viemClient.getCode({
address: wallet.account.address
// Fetch from RPC
console.log('FETCHING BALANCE FROM RPC')
const client = getClient()
const chain = client.chains.find((chain) => chain.id === chainId)
const rpcUrl = chain?.httpRpcUrl

if (!chain) {
return BigInt(0)
}

const viemClient = createPublicClient({
chain: chain?.viemChain,
transport: rpcUrl ? http(rpcUrl) : http()
})
} catch (getCodeError) {
throw getCodeError

try {
console.log('FETCHING BALANCE FROM RPC')
const balance = await viemClient.getBalance({ address })
console.log('BALANCE FETCHED FROM RPC')
// Cache the result
cacheEntry!.balance = { value: balance, timestamp: now }
return balance
} catch (error) {
return BigInt(0)
}
}

const hasCode = Boolean(code && code !== '0x')
const isEIP7702Delegated = Boolean(
code && code.toLowerCase().startsWith('0xef01')
)
const isSmartWallet =
// Get transaction count with caching
const getTransactionCount = async () => {
// Check cache first
console.log('CHECKING TRANSACTION COUNT CACHE')
if (
cacheEntry &&
cacheEntry?.txCount &&
now - cacheEntry?.txCount.timestamp < CACHE_DURATION_MS
) {
console.log('TRANSACTION COUNT CACHE HIT')
return cacheEntry?.txCount.value
}

// Fetch from RPC
console.log('FETCHING TRANSACTION COUNT FROM RPC')
const client = getClient()
const chain = client.chains.find((chain) => chain.id === chainId)
const rpcUrl = chain?.httpRpcUrl

if (!chain) {
return 0
}

const viemClient = createPublicClient({
chain: chain?.viemChain,
transport: rpcUrl ? http(rpcUrl) : http()
})

try {
const txCount = await viemClient.getTransactionCount({ address })
// Cache the result
cacheEntry!.txCount = { value: txCount, timestamp: now }
return txCount
} catch (error) {
return 0
}
}

const [
hasSmartWalletCapabilitiesResult,
getCodeResult,
nativeBalanceResult,
transactionCountResult
] = await Promise.allSettled([
getSmartWalletCapabilities(),
getCode(),
getNativeBalance(),
getTransactionCount()
])

const hasSmartWalletCapabilities =
hasSmartWalletCapabilitiesResult.status === 'fulfilled'
? hasSmartWalletCapabilitiesResult.value
: false
const { hasCode, isEIP7702Delegated } =
getCodeResult.status === 'fulfilled'
? getCodeResult.value
: { hasCode: false, isEIP7702Delegated: false }
const nativeBalance =
nativeBalanceResult.status === 'fulfilled'
? nativeBalanceResult.value
: BigInt(0)
const transactionCount =
transactionCountResult.status === 'fulfilled'
? transactionCountResult.value
: 0

let isSmartWallet =
hasSmartWalletCapabilities || hasCode || isEIP7702Delegated
const isEOA = !isSmartWallet

return { isEOA, isEIP7702Delegated }
// If balance is zero or transaction count is <= 1, it's likely a smart wallet
if (nativeBalance === BigInt(0) || transactionCount <= 1) {
isSmartWallet = true
}

return { isEOA: !isSmartWallet, isEIP7702Delegated }
} catch (error) {
return { isEOA: false, isEIP7702Delegated: false }
// On error, default to explicit deposit (isEOA: false) for safety
return {
isEOA: false,
isEIP7702Delegated: false
}
}
}
}
Expand Down
21 changes: 8 additions & 13 deletions packages/ui/src/components/widgets/SwapWidgetRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useIsWalletCompatible,
useFallbackState,
useGasTopUpRequired,
useEOADetection,
useExplicitDeposit,
useDisplayName,
useLighterAccount
} from '../../hooks/index.js'
Expand Down Expand Up @@ -480,20 +480,14 @@ const SwapWidgetRenderer: FC<SwapWidgetRendererProps> = ({

const isFromNative = fromToken?.address === fromChain?.currency?.address

const explicitDeposit = useEOADetection(
const explicitDeposit = useExplicitDeposit(
wallet,
fromToken?.chainId,
fromChain?.vmType,
fromChain,
address,
fromBalance,
isFromNative
address
)

const shouldSetQuoteParameters =
fromToken &&
toToken &&
(fromChain?.vmType !== 'evm' || explicitDeposit !== undefined)
const shouldSetQuoteParameters = fromToken && toToken

const quoteParameters: Parameters<typeof useQuote>['2'] =
shouldSetQuoteParameters
Expand Down Expand Up @@ -528,9 +522,10 @@ const SwapWidgetRenderer: FC<SwapWidgetRendererProps> = ({
}
}
: {}),
...(explicitDeposit !== undefined && {
explicitDeposit: explicitDeposit
})
...(explicitDeposit !== undefined &&
fromChain?.vmType === 'evm' && {
explicitDeposit: explicitDeposit
})
}
: undefined

Expand Down
Loading