@@ -7,7 +7,7 @@ import { ArrowRight, Check, ChevronDown, Info, Loader2, Search } from "lucide-re
77import { toast } from "sonner" ;
88import { prepareContractCall , prepareTransaction , sendTransaction , waitForReceipt } from "thirdweb" ;
99import { useActiveWallet , useActiveWalletChain } from "thirdweb/react" ;
10- import { formatUnits , maxUint256 , parseUnits , type Address , type Hex } from "viem" ;
10+ import { formatUnits , isAddress , maxUint256 , parseUnits , type Address , type Hex } from "viem" ;
1111import { Button } from "@/components/ui/button" ;
1212import { Checkbox } from "@/components/ui/checkbox" ;
1313import {
@@ -27,7 +27,7 @@ import { ensureOnChain, normalizeTxError } from "@/lib/thirdweb-tx";
2727import { cn } from "@/lib/utils" ;
2828import { getDefaultPair , NATIVE_TOKEN , type SwapToken } from "./chains" ;
2929import { useSwapChain } from "./SwapChainContext" ;
30- import { useWalletTokens } from "./useWalletTokens" ;
30+ import { useTokenLookup , useWalletTokens } from "./useWalletTokens" ;
3131import {
3232 formatBalanceDisplay ,
3333 useAllTokenBalances ,
@@ -192,6 +192,11 @@ function TokenPicker({
192192 tokens,
193193 } ) ;
194194
195+ // When the query looks like a contract address and nothing in the list
196+ // matches, resolve it via Alchemy metadata + Zora (on Base).
197+ const queryTrimmed = query . trim ( ) ;
198+ const lookup = useTokenLookup ( { address : queryTrimmed , chainId : chain . id } ) ;
199+
195200 // Show the first 4 tokens of the chain as the "popular" row — chain
196201 // registries are ordered to put the staples first.
197202 const popular = React . useMemo ( ( ) => tokens . slice ( 0 , 4 ) , [ tokens ] ) ;
@@ -295,9 +300,7 @@ function TokenPicker({
295300 ) }
296301
297302 < div className = "-mx-6 max-h-80 overflow-y-auto border-t" >
298- { filtered . length === 0 ? (
299- < p className = "py-8 text-center text-sm text-muted-foreground" > No tokens found</ p >
300- ) : (
303+ { filtered . length > 0 ? (
301304 filtered . map ( ( t ) => {
302305 const isSelected = t . address === value . address ;
303306 const isExcluded = t . address === exclude ;
@@ -335,6 +338,43 @@ function TokenPicker({
335338 </ button >
336339 ) ;
337340 } )
341+ ) : isAddress ( queryTrimmed ) ? (
342+ // Address pasted but not in token list — resolve it on-the-fly.
343+ lookup . isLoading ? (
344+ < p className = "flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground" >
345+ < Loader2 className = "h-4 w-4 animate-spin" />
346+ Resolving token…
347+ </ p >
348+ ) : lookup . data ? (
349+ < button
350+ type = "button"
351+ disabled = { lookup . data . address === exclude }
352+ onClick = { ( ) => choose ( lookup . data ! ) }
353+ className = { cn (
354+ "flex w-full items-center gap-3 px-6 py-2.5 text-left transition-colors" ,
355+ lookup . data . address === exclude
356+ ? "cursor-not-allowed opacity-40"
357+ : "hover:bg-accent" ,
358+ ) }
359+ >
360+ < TokenLogo token = { lookup . data } size = { 32 } chainId = { chain . id } />
361+ < div className = "min-w-0 flex-1" >
362+ < div className = "flex items-baseline gap-2" >
363+ < span className = "text-sm font-semibold" > { lookup . data . symbol } </ span >
364+ < span className = "truncate text-xs text-muted-foreground" > { lookup . data . name } </ span >
365+ </ div >
366+ < p className = "truncate font-mono text-[10px] text-muted-foreground" >
367+ { `${ lookup . data . address . slice ( 0 , 6 ) } …${ lookup . data . address . slice ( - 4 ) } ` }
368+ </ p >
369+ </ div >
370+ </ button >
371+ ) : (
372+ < p className = "py-8 text-center text-sm text-muted-foreground" >
373+ No ERC-20 token found at this address
374+ </ p >
375+ )
376+ ) : (
377+ < p className = "py-8 text-center text-sm text-muted-foreground" > No tokens found</ p >
338378 ) }
339379 </ div >
340380 </ div >
0 commit comments