1
1
import { useCallback , useMemo } from "react" ;
2
- import type { ThirdwebClient } from "thirdweb" ;
2
+ import {
3
+ NATIVE_TOKEN_ADDRESS ,
4
+ type ThirdwebClient ,
5
+ getAddress ,
6
+ } from "thirdweb" ;
3
7
import { shortenAddress } from "thirdweb/utils" ;
8
+ import { useAllChainsData } from "../../../hooks/chains/allChains" ;
4
9
import { useTokensData } from "../../../hooks/tokens/tokens" ;
5
10
import { replaceIpfsUrl } from "../../../lib/sdk" ;
6
11
import { fallbackChainIcon } from "../../../utils/chain-icons" ;
12
+ import type { TokenMetadata } from "../../api/universal-bridge/tokens" ;
7
13
import { cn } from "../../lib/utils" ;
8
14
import { Badge } from "../ui/badge" ;
9
15
import { Img } from "./Img" ;
10
16
import { SelectWithSearch } from "./select-with-search" ;
11
17
12
18
type Option = { label : string ; value : string } ;
13
19
20
+ const checksummedNativeTokenAddress = getAddress ( NATIVE_TOKEN_ADDRESS ) ;
21
+
14
22
export function TokenSelector ( props : {
15
- tokenAddress : string | undefined ;
16
- onChange : ( tokenAddress : string ) => void ;
23
+ selectedToken : { chainId : number ; address : string } | undefined ;
24
+ onChange : ( token : TokenMetadata ) => void ;
17
25
className ?: string ;
18
26
popoverContentClassName ?: string ;
19
27
chainId ?: number ;
20
28
side ?: "left" | "right" | "top" | "bottom" ;
21
- disableChainId ?: boolean ;
29
+ disableAddress ?: boolean ;
22
30
align ?: "center" | "start" | "end" ;
23
31
placeholder ?: string ;
24
32
client : ThirdwebClient ;
25
33
disabled ?: boolean ;
26
34
enabled ?: boolean ;
35
+ showCheck : boolean ;
36
+ addNativeTokenIfMissing : boolean ;
27
37
} ) {
28
- const { tokens , isFetching } = useTokensData ( {
38
+ const tokensQuery = useTokensData ( {
29
39
chainId : props . chainId ,
30
40
enabled : props . enabled ,
31
41
} ) ;
32
42
43
+ const { idToChain } = useAllChainsData ( ) ;
44
+
45
+ const tokens = useMemo ( ( ) => {
46
+ if ( ! tokensQuery . data ) {
47
+ return [ ] ;
48
+ }
49
+
50
+ if ( props . addNativeTokenIfMissing ) {
51
+ const hasNativeToken = tokensQuery . data . some (
52
+ ( token ) => token . address === checksummedNativeTokenAddress ,
53
+ ) ;
54
+
55
+ if ( ! hasNativeToken && props . chainId ) {
56
+ return [
57
+ {
58
+ name :
59
+ idToChain . get ( props . chainId ) ?. nativeCurrency . name ??
60
+ "Native Token" ,
61
+ symbol :
62
+ idToChain . get ( props . chainId ) ?. nativeCurrency . symbol ?? "ETH" ,
63
+ decimals : 18 ,
64
+ chainId : props . chainId ,
65
+ address : checksummedNativeTokenAddress ,
66
+ } satisfies TokenMetadata ,
67
+ ...tokensQuery . data ,
68
+ ] ;
69
+ }
70
+ }
71
+ return tokensQuery . data ;
72
+ } , [
73
+ tokensQuery . data ,
74
+ props . chainId ,
75
+ props . addNativeTokenIfMissing ,
76
+ idToChain ,
77
+ ] ) ;
78
+
79
+ const addressChainToToken = useMemo ( ( ) => {
80
+ const value = new Map < string , TokenMetadata > ( ) ;
81
+ for ( const token of tokens ) {
82
+ value . set ( `${ token . chainId } :${ token . address } ` , token ) ;
83
+ }
84
+ return value ;
85
+ } , [ tokens ] ) ;
86
+
33
87
const options = useMemo ( ( ) => {
34
- return tokens . allTokens . map ( ( token ) => {
35
- return {
36
- label : token . symbol ,
37
- value : `${ token . chainId } :${ token . address } ` ,
38
- } ;
39
- } ) ;
40
- } , [ tokens . allTokens ] ) ;
88
+ return (
89
+ tokens . map ( ( token ) => {
90
+ return {
91
+ label : token . symbol ,
92
+ value : `${ token . chainId } :${ token . address } ` ,
93
+ } ;
94
+ } ) || [ ]
95
+ ) ;
96
+ } , [ tokens ] ) ;
41
97
42
98
const searchFn = useCallback (
43
99
( option : Option , searchValue : string ) => {
44
- const token = tokens . addressChainToToken . get ( option . value ) ;
100
+ const token = addressChainToToken . get ( option . value ) ;
45
101
if ( ! token ) {
46
102
return false ;
47
103
}
@@ -55,12 +111,12 @@ export function TokenSelector(props: {
55
111
token . address . toLowerCase ( ) . includes ( searchValue . toLowerCase ( ) )
56
112
) ;
57
113
} ,
58
- [ tokens ] ,
114
+ [ addressChainToToken ] ,
59
115
) ;
60
116
61
117
const renderOption = useCallback (
62
118
( option : Option ) => {
63
- const token = tokens . addressChainToToken . get ( option . value ) ;
119
+ const token = addressChainToToken . get ( option . value ) ;
64
120
if ( ! token ) {
65
121
return option . label ;
66
122
}
@@ -87,36 +143,46 @@ export function TokenSelector(props: {
87
143
{ token . symbol }
88
144
</ span >
89
145
90
- { ! props . disableChainId && (
91
- < Badge variant = "outline" className = "gap-2 max-sm:hidden" >
146
+ { ! props . disableAddress && (
147
+ < Badge variant = "outline" className = "gap-2 py-1 max-sm:hidden" >
92
148
< span className = "text-muted-foreground" > Address</ span >
93
149
{ shortenAddress ( token . address , 4 ) }
94
150
</ Badge >
95
151
) }
96
152
</ div >
97
153
) ;
98
154
} ,
99
- [ tokens , props . disableChainId , props . client ] ,
155
+ [ addressChainToToken , props . disableAddress , props . client ] ,
100
156
) ;
101
157
158
+ const selectedValue = props . selectedToken
159
+ ? `${ props . selectedToken . chainId } :${ props . selectedToken . address } `
160
+ : undefined ;
161
+
102
162
return (
103
163
< SelectWithSearch
104
164
searchPlaceholder = "Search by name or symbol"
105
- value = { props . tokenAddress }
165
+ value = { selectedValue }
106
166
options = { options }
107
167
onValueChange = { ( tokenAddress ) => {
108
- props . onChange ( tokenAddress ) ;
168
+ const token = addressChainToToken . get ( tokenAddress ) ;
169
+ if ( ! token ) {
170
+ return ;
171
+ }
172
+ props . onChange ( token ) ;
109
173
} }
110
174
closeOnSelect = { true }
111
- showCheck = { false }
175
+ showCheck = { props . showCheck }
112
176
placeholder = {
113
- isFetching ? "Loading Tokens..." : props . placeholder || "Select Token"
177
+ tokensQuery . isPending
178
+ ? "Loading Tokens..."
179
+ : props . placeholder || "Select Token"
114
180
}
115
181
overrideSearchFn = { searchFn }
116
182
renderOption = { renderOption }
117
183
className = { props . className }
118
184
popoverContentClassName = { props . popoverContentClassName }
119
- disabled = { isFetching || props . disabled }
185
+ disabled = { tokensQuery . isPending || props . disabled }
120
186
side = { props . side }
121
187
align = { props . align }
122
188
/>
0 commit comments