Skip to content

Commit 56123ee

Browse files
authored
Refactor EOA detection (#915)
* Refactor EOA detection * feat: changeset
1 parent 499cd7f commit 56123ee

File tree

9 files changed

+324
-288
lines changed

9 files changed

+324
-288
lines changed

.changeset/witty-clocks-fall.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@relayprotocol/relay-sdk': minor
3+
'@relayprotocol/relay-kit-ui': minor
4+
---
5+
6+
Refactor EOA detection to improve ux

packages/sdk/src/types/AdaptedWallet.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ export type AdaptedWallet = {
116116
step: Execute['steps'][0]
117117
) => Promise<string | undefined>
118118
// detect if wallet is an EOA (externally owned account)
119-
isEOA?: (
120-
chainId: number
121-
) => Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }>
119+
// Note: returns isEOA: false when explicit deposit should be used
120+
isEOA?: (chainId: number) => Promise<{
121+
isEOA: boolean
122+
isEIP7702Delegated: boolean
123+
}>
122124
}

packages/sdk/src/utils/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export { request, APIError, isAPIError } from './request.js'
55
export { log, LogLevel } from './logger.js'
66
export { axios } from './axios.js'
77
export { default as prepareCallTransaction } from './prepareCallTransaction.js'
8-
export { adaptViemWallet } from './viemWallet.js'
8+
export { adaptViemWallet, isViemWalletClient } from './viemWallet.js'
99
export { convertViemChainToRelayChain, type RelayAPIChain } from './chain.js'
1010
export { getCurrentStepData } from './getCurrentStepData.js'
1111
export {

packages/sdk/src/utils/viemWallet.ts

Lines changed: 210 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ import {
1111
http
1212
} from 'viem'
1313

14+
// Cache for expensive RPC calls (code, balance, tx count)
15+
interface EOACacheEntry {
16+
code?: { value: string | undefined; timestamp: number }
17+
balance?: { value: bigint; timestamp: number }
18+
txCount?: { value: number; timestamp: number }
19+
}
20+
21+
const CACHE_DURATION_MS = 2 * 60 * 1000 // 2 minutes
22+
const eoaCache = new Map<string, EOACacheEntry>()
23+
24+
function getCacheKey(address: string, chainId: number): string {
25+
return `${address}-${chainId}`
26+
}
27+
1428
export function isViemWalletClient(
1529
wallet: WalletClient | AdaptedWallet
1630
): wallet is WalletClient {
@@ -245,60 +259,219 @@ export const adaptViemWallet = (wallet: WalletClient): AdaptedWallet => {
245259
},
246260
isEOA: async (
247261
chainId: number
248-
): Promise<{ isEOA: boolean; isEIP7702Delegated: boolean }> => {
262+
): Promise<{
263+
isEOA: boolean
264+
isEIP7702Delegated: boolean
265+
}> => {
249266
if (!wallet.account) {
250-
return { isEOA: false, isEIP7702Delegated: false }
267+
return {
268+
isEOA: false,
269+
isEIP7702Delegated: false
270+
}
251271
}
252272

253273
try {
254-
let hasSmartWalletCapabilities = false
255-
try {
256-
const capabilities = await wallet.getCapabilities({
257-
account: wallet.account,
258-
chainId
274+
const address = wallet.account.address
275+
const cacheKey = getCacheKey(address, chainId)
276+
const now = Date.now()
277+
278+
// Get or create cache entry for this address+chain
279+
let cacheEntry = eoaCache.get(cacheKey)
280+
if (!cacheEntry) {
281+
cacheEntry = {}
282+
eoaCache.set(cacheKey, cacheEntry)
283+
}
284+
285+
// Always fetch capabilities fresh (wallet-specific, not cacheable)
286+
const getSmartWalletCapabilities = async () => {
287+
let _hasSmartWalletCapabilities = false
288+
try {
289+
const capabilities = await wallet.getCapabilities({
290+
account: wallet.account,
291+
chainId
292+
})
293+
294+
_hasSmartWalletCapabilities = Boolean(
295+
capabilities?.atomicBatch?.supported ||
296+
capabilities?.paymasterService?.supported ||
297+
capabilities?.auxiliaryFunds?.supported ||
298+
capabilities?.sessionKeys?.supported
299+
)
300+
} catch (capabilitiesError) {}
301+
return _hasSmartWalletCapabilities
302+
}
303+
304+
// Get code with caching
305+
const getCode = async () => {
306+
// Check cache first
307+
console.log('CHECKING CODE CACHE')
308+
if (
309+
cacheEntry &&
310+
cacheEntry?.code &&
311+
now - cacheEntry?.code.timestamp < CACHE_DURATION_MS
312+
) {
313+
console.log('CODE CACHE HIT')
314+
const code = cacheEntry!.code.value
315+
const hasCode = Boolean(code && code !== '0x')
316+
const isEIP7702Delegated = Boolean(
317+
code && code.toLowerCase().startsWith('0xef01')
318+
)
319+
return { hasCode, isEIP7702Delegated }
320+
}
321+
322+
// Fetch from RPC
323+
console.log('FETCHING CODE FROM RPC')
324+
const client = getClient()
325+
const chain = client.chains.find((chain) => chain.id === chainId)
326+
const rpcUrl = chain?.httpRpcUrl
327+
328+
if (!chain) {
329+
throw new Error(`Chain ${chainId} not found in relay client`)
330+
}
331+
332+
const viemClient = createPublicClient({
333+
chain: chain?.viemChain,
334+
transport: rpcUrl ? http(rpcUrl) : http()
259335
})
260336

261-
hasSmartWalletCapabilities = Boolean(
262-
capabilities?.atomicBatch?.supported ||
263-
capabilities?.paymasterService?.supported ||
264-
capabilities?.auxiliaryFunds?.supported ||
265-
capabilities?.sessionKeys?.supported
266-
)
267-
} catch (capabilitiesError) {}
337+
try {
338+
const _code = await viemClient.getCode({ address })
268339

269-
const client = getClient()
270-
const chain = client.chains.find((chain) => chain.id === chainId)
271-
const rpcUrl = chain?.httpRpcUrl
340+
// Cache the result
341+
cacheEntry!.code = { value: _code, timestamp: now }
272342

273-
if (!chain) {
274-
throw new Error(`Chain ${chainId} not found in relay client`)
343+
const hasCode = Boolean(_code && _code !== '0x')
344+
const isEIP7702Delegated = Boolean(
345+
_code && _code.toLowerCase().startsWith('0xef01')
346+
)
347+
return { hasCode, isEIP7702Delegated }
348+
} catch (getCodeError) {
349+
throw getCodeError
350+
}
275351
}
276352

277-
const viemClient = createPublicClient({
278-
chain: chain?.viemChain,
279-
transport: rpcUrl ? http(rpcUrl) : http()
280-
})
353+
// Get balance with caching
354+
const getNativeBalance = async () => {
355+
// Check cache first
356+
console.log('CHECKING BALANCE CACHE')
357+
if (
358+
cacheEntry &&
359+
cacheEntry?.balance &&
360+
now - cacheEntry?.balance.timestamp < CACHE_DURATION_MS
361+
) {
362+
console.log('BALANCE CACHE HIT')
363+
return cacheEntry?.balance.value
364+
}
281365

282-
let code
283-
try {
284-
code = await viemClient.getCode({
285-
address: wallet.account.address
366+
// Fetch from RPC
367+
console.log('FETCHING BALANCE FROM RPC')
368+
const client = getClient()
369+
const chain = client.chains.find((chain) => chain.id === chainId)
370+
const rpcUrl = chain?.httpRpcUrl
371+
372+
if (!chain) {
373+
return BigInt(0)
374+
}
375+
376+
const viemClient = createPublicClient({
377+
chain: chain?.viemChain,
378+
transport: rpcUrl ? http(rpcUrl) : http()
286379
})
287-
} catch (getCodeError) {
288-
throw getCodeError
380+
381+
try {
382+
console.log('FETCHING BALANCE FROM RPC')
383+
const balance = await viemClient.getBalance({ address })
384+
console.log('BALANCE FETCHED FROM RPC')
385+
// Cache the result
386+
cacheEntry!.balance = { value: balance, timestamp: now }
387+
return balance
388+
} catch (error) {
389+
return BigInt(0)
390+
}
289391
}
290392

291-
const hasCode = Boolean(code && code !== '0x')
292-
const isEIP7702Delegated = Boolean(
293-
code && code.toLowerCase().startsWith('0xef01')
294-
)
295-
const isSmartWallet =
393+
// Get transaction count with caching
394+
const getTransactionCount = async () => {
395+
// Check cache first
396+
console.log('CHECKING TRANSACTION COUNT CACHE')
397+
if (
398+
cacheEntry &&
399+
cacheEntry?.txCount &&
400+
now - cacheEntry?.txCount.timestamp < CACHE_DURATION_MS
401+
) {
402+
console.log('TRANSACTION COUNT CACHE HIT')
403+
return cacheEntry?.txCount.value
404+
}
405+
406+
// Fetch from RPC
407+
console.log('FETCHING TRANSACTION COUNT FROM RPC')
408+
const client = getClient()
409+
const chain = client.chains.find((chain) => chain.id === chainId)
410+
const rpcUrl = chain?.httpRpcUrl
411+
412+
if (!chain) {
413+
return 0
414+
}
415+
416+
const viemClient = createPublicClient({
417+
chain: chain?.viemChain,
418+
transport: rpcUrl ? http(rpcUrl) : http()
419+
})
420+
421+
try {
422+
const txCount = await viemClient.getTransactionCount({ address })
423+
// Cache the result
424+
cacheEntry!.txCount = { value: txCount, timestamp: now }
425+
return txCount
426+
} catch (error) {
427+
return 0
428+
}
429+
}
430+
431+
const [
432+
hasSmartWalletCapabilitiesResult,
433+
getCodeResult,
434+
nativeBalanceResult,
435+
transactionCountResult
436+
] = await Promise.allSettled([
437+
getSmartWalletCapabilities(),
438+
getCode(),
439+
getNativeBalance(),
440+
getTransactionCount()
441+
])
442+
443+
const hasSmartWalletCapabilities =
444+
hasSmartWalletCapabilitiesResult.status === 'fulfilled'
445+
? hasSmartWalletCapabilitiesResult.value
446+
: false
447+
const { hasCode, isEIP7702Delegated } =
448+
getCodeResult.status === 'fulfilled'
449+
? getCodeResult.value
450+
: { hasCode: false, isEIP7702Delegated: false }
451+
const nativeBalance =
452+
nativeBalanceResult.status === 'fulfilled'
453+
? nativeBalanceResult.value
454+
: BigInt(0)
455+
const transactionCount =
456+
transactionCountResult.status === 'fulfilled'
457+
? transactionCountResult.value
458+
: 0
459+
460+
let isSmartWallet =
296461
hasSmartWalletCapabilities || hasCode || isEIP7702Delegated
297-
const isEOA = !isSmartWallet
298462

299-
return { isEOA, isEIP7702Delegated }
463+
// If balance is zero or transaction count is <= 1, it's likely a smart wallet
464+
if (nativeBalance === BigInt(0) || transactionCount <= 1) {
465+
isSmartWallet = true
466+
}
467+
468+
return { isEOA: !isSmartWallet, isEIP7702Delegated }
300469
} catch (error) {
301-
return { isEOA: false, isEIP7702Delegated: false }
470+
// On error, default to explicit deposit (isEOA: false) for safety
471+
return {
472+
isEOA: false,
473+
isEIP7702Delegated: false
474+
}
302475
}
303476
}
304477
}

packages/ui/src/components/widgets/SwapWidgetRenderer.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
useIsWalletCompatible,
1111
useFallbackState,
1212
useGasTopUpRequired,
13-
useEOADetection,
13+
useExplicitDeposit,
1414
useDisplayName,
1515
useLighterAccount
1616
} from '../../hooks/index.js'
@@ -480,20 +480,14 @@ const SwapWidgetRenderer: FC<SwapWidgetRendererProps> = ({
480480

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

483-
const explicitDeposit = useEOADetection(
483+
const explicitDeposit = useExplicitDeposit(
484484
wallet,
485485
fromToken?.chainId,
486486
fromChain?.vmType,
487-
fromChain,
488-
address,
489-
fromBalance,
490-
isFromNative
487+
address
491488
)
492489

493-
const shouldSetQuoteParameters =
494-
fromToken &&
495-
toToken &&
496-
(fromChain?.vmType !== 'evm' || explicitDeposit !== undefined)
490+
const shouldSetQuoteParameters = fromToken && toToken
497491

498492
const quoteParameters: Parameters<typeof useQuote>['2'] =
499493
shouldSetQuoteParameters
@@ -528,9 +522,10 @@ const SwapWidgetRenderer: FC<SwapWidgetRendererProps> = ({
528522
}
529523
}
530524
: {}),
531-
...(explicitDeposit !== undefined && {
532-
explicitDeposit: explicitDeposit
533-
})
525+
...(explicitDeposit !== undefined &&
526+
fromChain?.vmType === 'evm' && {
527+
explicitDeposit: explicitDeposit
528+
})
534529
}
535530
: undefined
536531

0 commit comments

Comments
 (0)