@xswap-link/sdk 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.d.mts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.global.js +87 -86
- package/dist/index.js +2888 -2521
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2828 -2458
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +5 -3
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +16 -0
- package/src/assets/icons/CaretDownIcon.tsx +12 -0
- package/src/assets/icons/CaretsUpDownIcon.tsx +13 -0
- package/src/assets/icons/ChainlinkMarkIcon.tsx +17 -0
- package/src/assets/icons/ClockIcon.tsx +14 -0
- package/src/assets/icons/CloseIcon.tsx +6 -2
- package/src/assets/icons/WarningIcon.tsx +19 -0
- package/src/assets/icons/index.ts +6 -0
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +12 -5
- package/src/components/Swap/Header/Controls/index.tsx +50 -36
- package/src/components/Swap/Header/index.tsx +12 -5
- package/src/components/Swap/HistoryView/ActivityCard/index.tsx +212 -0
- package/src/components/Swap/HistoryView/index.tsx +24 -6
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +5 -7
- package/src/components/Swap/SettingsView/Delivery/index.tsx +27 -28
- package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +27 -24
- package/src/components/Swap/SettingsView/Slippage/index.tsx +44 -43
- package/src/components/Swap/SettingsView/index.tsx +1 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +50 -4
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +9 -19
- package/src/components/Swap/SwapView/FeesPanel/Fee/index.tsx +4 -4
- package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +5 -9
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +21 -22
- package/src/components/Swap/SwapView/SwapButton/index.tsx +25 -6
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +56 -12
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +42 -41
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +159 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +105 -55
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +226 -276
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +57 -97
- package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +27 -0
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +60 -49
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +20 -25
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +6 -1
- package/src/components/Swap/SwapView/index.tsx +2 -1
- package/src/components/Swap/index.tsx +20 -4
- package/src/components/ToggleButton/index.tsx +18 -15
- package/src/components/TxConfigForm/index.tsx +5 -5
- package/src/components/TxWidgetWC/index.tsx +4 -0
- package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
- package/src/context/HistoryProvider.tsx +2 -1
- package/src/context/SwapProvider.tsx +87 -62
- package/src/context/TxUIWrapper.tsx +3 -3
- package/src/hooks/useEvmContractApi.ts +28 -4
- package/src/models/TransactionHistory.ts +1 -0
- package/src/models/payloads/GetBalancesPayload.ts +1 -0
- package/src/models/payloads/ModalIntegrationPayload.ts +1 -0
- package/src/models/payloads/WidgetIntegrationPayload.ts +1 -0
- package/src/services/api.ts +37 -0
- package/src/utils/index.ts +48 -1
- package/src/utils/tokens.ts +20 -1
- package/tailwind.config.js +4 -1
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +0 -97
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/index.tsx +0 -103
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/index.tsx +0 -65
- package/src/components/Swap/SwapView/SwapPanel/MaxPanel/index.tsx +0 -43
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/QuickPickTokenItem/index.tsx +0 -37
|
@@ -4,16 +4,15 @@ import { CloseIcon, CopyIcon, SearchIcon } from "@src/assets/icons";
|
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
Spinner,
|
|
7
|
-
TextInput,
|
|
8
7
|
TokenLogoWithChain,
|
|
9
8
|
UnknownTokenLogo,
|
|
10
9
|
} from "@src/components";
|
|
11
10
|
import { SwapPanelType } from "@src/components/Swap/SwapView";
|
|
12
11
|
import { useSwapContext } from "@src/context";
|
|
13
|
-
import { ERC20Abi } from "@src/contracts";
|
|
14
|
-
import { useEvmContractApi } from "@src/hooks";
|
|
15
12
|
import { Chain, Ecosystem, ImportedTokenData, TokenOption } from "@src/models";
|
|
16
13
|
import {
|
|
14
|
+
isBridgeTokenConnectionSupported,
|
|
15
|
+
isChainConnectionSupported,
|
|
17
16
|
isETHAddressValid,
|
|
18
17
|
isServer,
|
|
19
18
|
isSolanaAddressValid,
|
|
@@ -22,12 +21,13 @@ import {
|
|
|
22
21
|
import { BigNumber, ethers } from "ethers";
|
|
23
22
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
24
23
|
import { GroupedVirtuoso } from "react-virtuoso";
|
|
25
|
-
import {
|
|
24
|
+
import { NetworkFilter } from "./NetworkFilter";
|
|
26
25
|
import { TokenItem } from "./TokenItem";
|
|
27
26
|
import { useAccount } from "wagmi";
|
|
28
27
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
29
28
|
import { METADATA_PROGRAM_ID } from "@src/constants";
|
|
30
29
|
import { useWallet } from "@src/context/WalletProvider";
|
|
30
|
+
import { getTokenMetadata } from "@src/services/api";
|
|
31
31
|
|
|
32
32
|
type Props = {
|
|
33
33
|
type: SwapPanelType;
|
|
@@ -55,31 +55,38 @@ export const TokenPicker = ({
|
|
|
55
55
|
const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
|
|
56
56
|
const [customTokenData, setCustomTokenData] =
|
|
57
57
|
useState<ImportedTokenData | null>(null);
|
|
58
|
-
const [
|
|
58
|
+
const [networkFilter, setNetworkFilter] = useState<Chain | undefined>(chain);
|
|
59
59
|
const currentRouteRequestNonce = useRef<number>(0);
|
|
60
60
|
const tokenSearchRef = useRef<HTMLInputElement | null>(null);
|
|
61
61
|
|
|
62
62
|
const {
|
|
63
63
|
bridgeUI,
|
|
64
64
|
supportedChains,
|
|
65
|
+
srcChain,
|
|
66
|
+
dstChain,
|
|
65
67
|
findTokenDataByAddressAndChain,
|
|
66
68
|
tokenPrices,
|
|
67
69
|
addTokenToChainIfNotExists,
|
|
70
|
+
bridgeTokensDictionary,
|
|
68
71
|
} = useSwapContext();
|
|
69
72
|
const { address } = useAccount();
|
|
70
73
|
const {
|
|
71
74
|
solana: { address: solanaAddress },
|
|
72
75
|
} = useWallet();
|
|
73
|
-
const evmContractApi = useEvmContractApi();
|
|
74
76
|
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
setShowImportWarning(false);
|
|
77
79
|
setAcceptRisks(false);
|
|
78
80
|
}, [isOpen]);
|
|
79
81
|
|
|
82
|
+
// Custom token imports follow the network filter, not the chain currently
|
|
83
|
+
// set on the swap panel — the picker lists tokens across networks, so a
|
|
84
|
+
// pasted address is resolved on the network the user is browsing.
|
|
85
|
+
const importChain = networkFilter ?? chain;
|
|
86
|
+
|
|
80
87
|
const importCustomToken = useCallback(() => {
|
|
81
88
|
if (
|
|
82
|
-
|
|
89
|
+
importChain &&
|
|
83
90
|
customTokenData &&
|
|
84
91
|
(isETHAddressValid(searchValue) || isSolanaAddressValid(searchValue))
|
|
85
92
|
) {
|
|
@@ -99,12 +106,12 @@ export const TokenPicker = ({
|
|
|
99
106
|
quickPick: false,
|
|
100
107
|
supported: true,
|
|
101
108
|
priority: 0,
|
|
102
|
-
chainId:
|
|
109
|
+
chainId: importChain.chainId,
|
|
103
110
|
};
|
|
104
111
|
|
|
105
112
|
if (!isServer) {
|
|
106
|
-
customTokens[
|
|
107
|
-
...(customTokens[
|
|
113
|
+
customTokens[importChain.chainId] = {
|
|
114
|
+
...(customTokens[importChain.chainId] || {}),
|
|
108
115
|
[searchValue.toLowerCase()]: newCustomToken,
|
|
109
116
|
};
|
|
110
117
|
localStorage.setItem("custom-tokens", JSON.stringify(customTokens));
|
|
@@ -115,7 +122,7 @@ export const TokenPicker = ({
|
|
|
115
122
|
setModalOpen(false);
|
|
116
123
|
}
|
|
117
124
|
}, [
|
|
118
|
-
|
|
125
|
+
importChain,
|
|
119
126
|
customTokenData,
|
|
120
127
|
searchValue,
|
|
121
128
|
addTokenToChainIfNotExists,
|
|
@@ -123,42 +130,32 @@ export const TokenPicker = ({
|
|
|
123
130
|
]);
|
|
124
131
|
|
|
125
132
|
const getCustomTokenData = useCallback(async () => {
|
|
133
|
+
if (!importChain) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
126
136
|
const nonce = Date.now();
|
|
127
137
|
currentRouteRequestNonce.current = nonce;
|
|
128
|
-
const rpcUrl = supportedChains.find(
|
|
129
|
-
({ ecosystem, chainId }) =>
|
|
130
|
-
ecosystem === Ecosystem.EVM && chainId === chain?.chainId,
|
|
131
|
-
)?.publicRpcUrls[0];
|
|
132
|
-
const customTokenContract = new ethers.Contract(
|
|
133
|
-
searchValue.toLowerCase(),
|
|
134
|
-
ERC20Abi,
|
|
135
|
-
ethers.getDefaultProvider(rpcUrl),
|
|
136
|
-
);
|
|
137
138
|
setLoadingCustomTokenData(true);
|
|
138
139
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
// Resolved server-side through private RPCs — public ones often 429
|
|
141
|
+
const tokenData = await getTokenMetadata({
|
|
142
|
+
chainId: importChain.chainId,
|
|
143
|
+
tokenAddress: searchValue.toLowerCase(),
|
|
144
|
+
});
|
|
144
145
|
if (nonce !== currentRouteRequestNonce.current) {
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
setCustomTokenData({
|
|
149
|
-
address: searchValue.toLowerCase(),
|
|
150
|
-
decimals,
|
|
151
|
-
name,
|
|
152
|
-
symbol,
|
|
153
|
-
});
|
|
148
|
+
setCustomTokenData(tokenData);
|
|
154
149
|
} catch (_) {
|
|
155
|
-
|
|
150
|
+
if (nonce === currentRouteRequestNonce.current) {
|
|
151
|
+
setCustomTokenData(null);
|
|
152
|
+
}
|
|
156
153
|
} finally {
|
|
157
154
|
if (nonce === currentRouteRequestNonce.current) {
|
|
158
155
|
setLoadingCustomTokenData(false);
|
|
159
156
|
}
|
|
160
157
|
}
|
|
161
|
-
}, [
|
|
158
|
+
}, [importChain, searchValue]);
|
|
162
159
|
|
|
163
160
|
const getCustomSolanaTokenData = useCallback(async () => {
|
|
164
161
|
const nonce = Date.now();
|
|
@@ -166,7 +163,7 @@ export const TokenPicker = ({
|
|
|
166
163
|
|
|
167
164
|
const rpcUrl = supportedChains.find(
|
|
168
165
|
({ ecosystem, chainId }) =>
|
|
169
|
-
ecosystem === Ecosystem.SOLANA && chainId ===
|
|
166
|
+
ecosystem === Ecosystem.SOLANA && chainId === importChain?.chainId,
|
|
170
167
|
)?.publicRpcUrls[0];
|
|
171
168
|
|
|
172
169
|
if (!rpcUrl || !searchValue || !isSolanaAddressValid(searchValue)) {
|
|
@@ -256,23 +253,26 @@ export const TokenPicker = ({
|
|
|
256
253
|
setLoadingCustomTokenData(false);
|
|
257
254
|
}
|
|
258
255
|
}
|
|
259
|
-
}, [
|
|
256
|
+
}, [importChain, searchValue, supportedChains]);
|
|
260
257
|
|
|
261
258
|
useEffect(() => {
|
|
262
259
|
if (
|
|
263
260
|
!bridgeUI &&
|
|
264
|
-
|
|
261
|
+
importChain &&
|
|
265
262
|
!findTokenDataByAddressAndChain(
|
|
266
|
-
|
|
263
|
+
importChain.ecosystem === Ecosystem.EVM
|
|
267
264
|
? searchValue.toLowerCase()
|
|
268
265
|
: searchValue,
|
|
269
|
-
|
|
266
|
+
importChain.chainId,
|
|
270
267
|
)
|
|
271
268
|
) {
|
|
272
|
-
if (
|
|
269
|
+
if (
|
|
270
|
+
importChain.ecosystem === Ecosystem.EVM &&
|
|
271
|
+
isETHAddressValid(searchValue)
|
|
272
|
+
) {
|
|
273
273
|
getCustomTokenData();
|
|
274
274
|
} else if (
|
|
275
|
-
|
|
275
|
+
importChain.ecosystem === Ecosystem.SOLANA &&
|
|
276
276
|
isSolanaAddressValid(searchValue)
|
|
277
277
|
) {
|
|
278
278
|
getCustomSolanaTokenData();
|
|
@@ -290,45 +290,31 @@ export const TokenPicker = ({
|
|
|
290
290
|
getCustomTokenData,
|
|
291
291
|
getCustomSolanaTokenData,
|
|
292
292
|
findTokenDataByAddressAndChain,
|
|
293
|
-
|
|
293
|
+
importChain,
|
|
294
294
|
]);
|
|
295
295
|
|
|
296
296
|
useEffect(() => {
|
|
297
297
|
setSearchValue("");
|
|
298
|
+
setNetworkFilter(chain);
|
|
299
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
298
300
|
}, [isOpen]);
|
|
299
301
|
|
|
300
302
|
const isMatch = (value, searchValue) =>
|
|
301
303
|
value.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
|
|
302
304
|
|
|
303
|
-
const
|
|
304
|
-
()
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
otherTokens?.filter(
|
|
317
|
-
(token) =>
|
|
318
|
-
isMatch(token.symbol, searchValue) ||
|
|
319
|
-
isMatch(token.name, searchValue) ||
|
|
320
|
-
token.address.toLowerCase() === searchValue.toLowerCase(),
|
|
321
|
-
),
|
|
322
|
-
[searchValue, otherTokens],
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
useEffect(() => {
|
|
326
|
-
if (isOpen && tokenSearchRef.current) {
|
|
327
|
-
setTimeout(() => {
|
|
328
|
-
tokenSearchRef.current?.focus();
|
|
329
|
-
}, 10); // delay focus a tiny bit to ensure it works
|
|
330
|
-
}
|
|
331
|
-
}, [isOpen]);
|
|
305
|
+
const visibleTokens = useMemo(() => {
|
|
306
|
+
const all = [...(tokens || []), ...(otherTokens || [])];
|
|
307
|
+
return all.filter((token) => {
|
|
308
|
+
if (networkFilter && token.chainId !== networkFilter.chainId) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
return (
|
|
312
|
+
isMatch(token.symbol, searchValue) ||
|
|
313
|
+
isMatch(token.name, searchValue) ||
|
|
314
|
+
token.address.toLowerCase() === searchValue.toLowerCase()
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
}, [tokens, otherTokens, searchValue, networkFilter]);
|
|
332
318
|
|
|
333
319
|
const calculateTokenValue = useCallback(
|
|
334
320
|
(token: TokenOption) => {
|
|
@@ -342,119 +328,134 @@ export const TokenPicker = ({
|
|
|
342
328
|
[tokenPrices],
|
|
343
329
|
);
|
|
344
330
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const valueB = calculateTokenValue(tokenB);
|
|
356
|
-
// First sort by USD value
|
|
357
|
-
if (valueB !== valueA) {
|
|
358
|
-
return valueB - valueA;
|
|
331
|
+
const getResetWarning = useCallback(
|
|
332
|
+
(token: TokenOption): SwapPanelType | null => {
|
|
333
|
+
if (type === "destination") {
|
|
334
|
+
if (
|
|
335
|
+
srcChain &&
|
|
336
|
+
!isChainConnectionSupported(srcChain, token.chainId, bridgeUI)
|
|
337
|
+
) {
|
|
338
|
+
return "source";
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
359
341
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
});
|
|
363
|
-
}, [filteredTokens, otherFilteredTokens, calculateTokenValue]);
|
|
364
|
-
|
|
365
|
-
const allTokens = useMemo(() => {
|
|
366
|
-
const currentChainTokens = filteredTokens
|
|
367
|
-
.filter((token) => BigNumber.from(token?.balance || "0").eq(0))
|
|
368
|
-
.sort(
|
|
369
|
-
(tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
|
|
370
|
-
);
|
|
371
|
-
const otherChainTokens = otherFilteredTokens
|
|
372
|
-
.filter((token) => BigNumber.from(token?.balance || "0").eq(0))
|
|
373
|
-
.sort(
|
|
374
|
-
(tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
|
|
342
|
+
const tokenChain = supportedChains.find(
|
|
343
|
+
({ chainId }) => chainId === token.chainId,
|
|
375
344
|
);
|
|
345
|
+
if (
|
|
346
|
+
dstChain &&
|
|
347
|
+
tokenChain &&
|
|
348
|
+
(!isChainConnectionSupported(tokenChain, dstChain.chainId, bridgeUI) ||
|
|
349
|
+
(bridgeUI &&
|
|
350
|
+
!isBridgeTokenConnectionSupported({
|
|
351
|
+
token,
|
|
352
|
+
tokenChain,
|
|
353
|
+
dstChainId: dstChain.chainId,
|
|
354
|
+
bridgeTokensDictionary,
|
|
355
|
+
})))
|
|
356
|
+
) {
|
|
357
|
+
return "destination";
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
},
|
|
361
|
+
[type, srcChain, dstChain, bridgeUI, supportedChains, bridgeTokensDictionary],
|
|
362
|
+
);
|
|
376
363
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
364
|
+
// Tokens that reset the other side of the swap on selection go below
|
|
365
|
+
// the ones that can be picked freely.
|
|
366
|
+
const compareByResetWarning = useCallback(
|
|
367
|
+
(tokenA: TokenOption, tokenB: TokenOption) =>
|
|
368
|
+
Number(!!getResetWarning(tokenA)) - Number(!!getResetWarning(tokenB)),
|
|
369
|
+
[getResetWarning],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const ownedTokens = useMemo(() => {
|
|
373
|
+
if (!address && !solanaAddress) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
return visibleTokens
|
|
377
|
+
.filter((token) => BigNumber.from(token?.balance || "0").gt(0))
|
|
378
|
+
.sort((tokenA, tokenB) => {
|
|
379
|
+
const warningOrder = compareByResetWarning(tokenA, tokenB);
|
|
380
|
+
if (warningOrder !== 0) {
|
|
381
|
+
return warningOrder;
|
|
382
|
+
}
|
|
383
|
+
const valueA = calculateTokenValue(tokenA);
|
|
384
|
+
const valueB = calculateTokenValue(tokenB);
|
|
385
|
+
// First sort by USD value
|
|
386
|
+
if (valueB !== valueA) {
|
|
387
|
+
return valueB - valueA;
|
|
388
|
+
}
|
|
389
|
+
// If USD values are equal (or both 0), sort by priority
|
|
390
|
+
return tokenB.priority - tokenA.priority;
|
|
391
|
+
});
|
|
394
392
|
}, [
|
|
395
|
-
|
|
396
|
-
otherFilteredTokens,
|
|
397
|
-
ownedFilteredTokens,
|
|
398
|
-
chain,
|
|
393
|
+
visibleTokens,
|
|
399
394
|
address,
|
|
400
395
|
solanaAddress,
|
|
396
|
+
calculateTokenValue,
|
|
397
|
+
compareByResetWarning,
|
|
401
398
|
]);
|
|
402
399
|
|
|
403
|
-
const
|
|
404
|
-
() =>
|
|
405
|
-
|
|
400
|
+
const recommendedTokens = useMemo(
|
|
401
|
+
() =>
|
|
402
|
+
visibleTokens
|
|
403
|
+
.filter(
|
|
404
|
+
(token) =>
|
|
405
|
+
!(address || solanaAddress) ||
|
|
406
|
+
BigNumber.from(token?.balance || "0").eq(0),
|
|
407
|
+
)
|
|
408
|
+
.sort(
|
|
409
|
+
(tokenDataA, tokenDataB) =>
|
|
410
|
+
compareByResetWarning(tokenDataA, tokenDataB) ||
|
|
411
|
+
tokenDataB.priority - tokenDataA.priority,
|
|
412
|
+
),
|
|
413
|
+
[visibleTokens, address, solanaAddress, compareByResetWarning],
|
|
406
414
|
);
|
|
407
415
|
|
|
408
416
|
const tokenGroups = useMemo(() => {
|
|
409
|
-
|
|
410
|
-
.filter((token) => BigNumber.from(token?.balance || "0").eq(0))
|
|
411
|
-
.sort(
|
|
412
|
-
(tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
|
|
413
|
-
);
|
|
414
|
-
const otherChainTokens = otherFilteredTokens
|
|
415
|
-
.filter((token) => BigNumber.from(token?.balance || "0").eq(0))
|
|
416
|
-
.sort(
|
|
417
|
-
(tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
return chain
|
|
417
|
+
return ownedTokens.length
|
|
421
418
|
? [
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
: []),
|
|
425
|
-
currentChainTokens,
|
|
426
|
-
otherChainTokens,
|
|
419
|
+
{ label: "MY TOKENS.", tokens: ownedTokens },
|
|
420
|
+
{ label: "RECOMMENDED.", tokens: recommendedTokens },
|
|
427
421
|
]
|
|
428
|
-
: [
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
422
|
+
: [{ label: "RECOMMENDED.", tokens: recommendedTokens }];
|
|
423
|
+
}, [ownedTokens, recommendedTokens]);
|
|
424
|
+
|
|
425
|
+
const allTokens = useMemo(
|
|
426
|
+
() => tokenGroups.flatMap((group) => group.tokens),
|
|
427
|
+
[tokenGroups],
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
useEffect(() => {
|
|
431
|
+
if (isOpen && tokenSearchRef.current) {
|
|
432
|
+
setTimeout(() => {
|
|
433
|
+
tokenSearchRef.current?.focus();
|
|
434
|
+
}, 10); // delay focus a tiny bit to ensure it works
|
|
435
|
+
}
|
|
436
|
+
}, [isOpen]);
|
|
442
437
|
|
|
443
438
|
return showImportWarning ? (
|
|
444
439
|
<>
|
|
445
|
-
<div className="flex justify-between">
|
|
446
|
-
<div className="text-
|
|
447
|
-
|
|
448
|
-
|
|
440
|
+
<div className="flex justify-between items-center mb-6">
|
|
441
|
+
<div className="text-2xl leading-8 tracking-[0.01em]">
|
|
442
|
+
Import token
|
|
443
|
+
</div>
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
aria-label="Close"
|
|
447
|
+
className="flex items-center justify-center w-8 h-8 rounded-md bg-t_bg_tertiary bg-opacity-5 hover:bg-opacity-10 transition-colors"
|
|
449
448
|
onClick={() => setModalOpen(false)}
|
|
450
449
|
>
|
|
451
|
-
<
|
|
452
|
-
|
|
450
|
+
<div className="w-4 h-4 opacity-50 fill-t_text_primary flex items-center justify-center">
|
|
451
|
+
<CloseIcon />
|
|
452
|
+
</div>
|
|
453
|
+
</button>
|
|
453
454
|
</div>
|
|
454
455
|
<div className="flex flex-col h-full gap-2">
|
|
455
456
|
{customTokenData && (
|
|
456
457
|
<div
|
|
457
|
-
className={`
|
|
458
|
+
className={`flex gap-3 py-3 px-4 items-center grow-0 bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-10 rounded-xl`}
|
|
458
459
|
>
|
|
459
460
|
<UnknownTokenLogo
|
|
460
461
|
tokenName={customTokenData.name}
|
|
@@ -464,7 +465,7 @@ export const TokenPicker = ({
|
|
|
464
465
|
<div className="overflow-hidden whitespace-nowrap text-ellipsis text-sm font-medium">
|
|
465
466
|
{customTokenData.name}
|
|
466
467
|
</div>
|
|
467
|
-
<div className="
|
|
468
|
+
<div className="text-sm text-t_text_primary text-opacity-50">
|
|
468
469
|
{customTokenData.symbol}
|
|
469
470
|
</div>
|
|
470
471
|
</div>
|
|
@@ -478,19 +479,19 @@ export const TokenPicker = ({
|
|
|
478
479
|
)
|
|
479
480
|
}
|
|
480
481
|
aria-label={`Copy ${customTokenData.symbol} token address`}
|
|
481
|
-
className="flex items-center p-2 fill-
|
|
482
|
+
className="flex items-center p-2 fill-t_text_primary cursor-pointer hover:fill-t_main_accent_light"
|
|
482
483
|
>
|
|
483
484
|
<CopyIcon className="w-5 h-5" />
|
|
484
485
|
</button>
|
|
485
486
|
</div>
|
|
486
487
|
</div>
|
|
487
488
|
)}
|
|
488
|
-
<div className="px-
|
|
489
|
+
<div className="px-4 py-4 rounded-xl border border-solid border-t_warning_dark text-sm text-t_text_primary text-opacity-80">
|
|
489
490
|
Ensure that you have verified the token address and confirmed its
|
|
490
491
|
correctness
|
|
491
492
|
</div>
|
|
492
|
-
<div className="bg-
|
|
493
|
-
<div className="text-sm mb-2">
|
|
493
|
+
<div className="bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-10 rounded-xl p-4">
|
|
494
|
+
<div className="text-sm mb-2 text-t_text_primary text-opacity-80">
|
|
494
495
|
Be mindful that anyone can create tokens, even counterfeit versions
|
|
495
496
|
of existing ones. Certain tokens and their technical specifications
|
|
496
497
|
might not be compatible with XSwap services. By importing such
|
|
@@ -501,21 +502,21 @@ export const TokenPicker = ({
|
|
|
501
502
|
onClick={() => setAcceptRisks((state) => !state)}
|
|
502
503
|
className="flex items-center"
|
|
503
504
|
>
|
|
504
|
-
<div className="flex justify-center items-center p-1 w-6 h-6 border-2 border-
|
|
505
|
+
<div className="flex justify-center items-center p-1 w-6 h-6 border-2 border-solid border-t_text_primary border-opacity-50 rounded-md mr-2">
|
|
505
506
|
{acceptRisks && (
|
|
506
507
|
<FontAwesomeIcon icon={faCheck} className="w-4 h-4" />
|
|
507
508
|
)}
|
|
508
509
|
</div>
|
|
509
510
|
|
|
510
|
-
<div className="font-
|
|
511
|
+
<div className="font-medium">Accept</div>
|
|
511
512
|
</button>
|
|
512
513
|
</div>
|
|
513
514
|
<div className="flex grow items-end justify-center">
|
|
514
515
|
<Button
|
|
515
516
|
type="button"
|
|
516
517
|
disabled={!acceptRisks}
|
|
517
|
-
className={`
|
|
518
|
-
!acceptRisks ? "!cursor-not-allowed" : ""
|
|
518
|
+
className={`w-full !rounded-[10px] !py-3 !text-sm !font-medium bg-gradient-to-br from-t_main_accent_light to-t_main_accent_dark !text-t_button_pr_text ${
|
|
519
|
+
!acceptRisks ? "!cursor-not-allowed opacity-50" : ""
|
|
519
520
|
}`}
|
|
520
521
|
onClick={importCustomToken}
|
|
521
522
|
>
|
|
@@ -526,49 +527,43 @@ export const TokenPicker = ({
|
|
|
526
527
|
</>
|
|
527
528
|
) : (
|
|
528
529
|
<>
|
|
529
|
-
<div className="flex justify-between">
|
|
530
|
-
<div className="text-
|
|
531
|
-
|
|
532
|
-
|
|
530
|
+
<div className="flex justify-between items-center mb-6">
|
|
531
|
+
<div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
|
|
532
|
+
Select Token
|
|
533
|
+
</div>
|
|
534
|
+
<button
|
|
535
|
+
type="button"
|
|
536
|
+
aria-label="Close"
|
|
537
|
+
className="flex items-center justify-center w-8 h-8 rounded-md bg-t_bg_tertiary bg-opacity-5 hover:bg-opacity-10 transition-colors"
|
|
533
538
|
onClick={() => setModalOpen(false)}
|
|
534
539
|
>
|
|
535
|
-
<
|
|
536
|
-
|
|
540
|
+
<div className="w-4 h-4 opacity-50 fill-t_text_primary flex items-center justify-center">
|
|
541
|
+
<CloseIcon />
|
|
542
|
+
</div>
|
|
543
|
+
</button>
|
|
537
544
|
</div>
|
|
538
545
|
|
|
539
|
-
<
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
<div className="fill-t_text_primary">
|
|
546
|
+
<div className="flex items-center justify-between gap-2 h-14 pl-4 pr-1 rounded-xl bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-5 mb-6">
|
|
547
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
548
|
+
<div className="fill-t_text_primary opacity-50 [&_svg]:w-4 [&_svg]:h-4 flex items-center shrink-0">
|
|
543
549
|
<SearchIcon />
|
|
544
550
|
</div>
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
.filter((token) => token?.quickPick)
|
|
559
|
-
.map((token) => (
|
|
560
|
-
<QuickPickTokenItem
|
|
561
|
-
key={token.address}
|
|
562
|
-
token={token}
|
|
563
|
-
selectedTokenAddress={selectedTokenAddress}
|
|
564
|
-
onClick={() => {
|
|
565
|
-
onSelect(token);
|
|
566
|
-
setModalOpen(false);
|
|
567
|
-
}}
|
|
568
|
-
/>
|
|
569
|
-
))}
|
|
551
|
+
<input
|
|
552
|
+
ref={tokenSearchRef}
|
|
553
|
+
value={searchValue}
|
|
554
|
+
onChange={(e) => setSearchValue(e.target.value)}
|
|
555
|
+
placeholder="Search name or paste address"
|
|
556
|
+
className="bg-transparent border-none outline-none w-full text-sm leading-6 text-t_text_primary placeholder:text-t_text_primary placeholder:text-opacity-40"
|
|
557
|
+
/>
|
|
558
|
+
</div>
|
|
559
|
+
<NetworkFilter
|
|
560
|
+
type={type}
|
|
561
|
+
selectedChain={networkFilter}
|
|
562
|
+
onSelect={setNetworkFilter}
|
|
563
|
+
/>
|
|
570
564
|
</div>
|
|
571
|
-
|
|
565
|
+
|
|
566
|
+
<div className="flex flex-col mx-0 h-full min-h-0 flex-1">
|
|
572
567
|
{loadingCustomTokenData ? (
|
|
573
568
|
<div className="flex justify-center w-full py-4 px-0">
|
|
574
569
|
<Spinner className="w-9 h-9" />
|
|
@@ -576,10 +571,10 @@ export const TokenPicker = ({
|
|
|
576
571
|
) : (
|
|
577
572
|
<>
|
|
578
573
|
{customTokenData ? (
|
|
579
|
-
<div className=
|
|
574
|
+
<div className="flex gap-3 py-3 px-4 items-center rounded-xl bg-t_bg_tertiary bg-opacity-[0.03]">
|
|
580
575
|
<TokenLogoWithChain
|
|
581
576
|
token={customTokenData}
|
|
582
|
-
chain={
|
|
577
|
+
chain={importChain}
|
|
583
578
|
tokenLogoClassName="w-9 h-9"
|
|
584
579
|
generatedLogoClassName="w-9 h-9 text-[16px]"
|
|
585
580
|
chainLogoClassName="w-5"
|
|
@@ -590,17 +585,17 @@ export const TokenPicker = ({
|
|
|
590
585
|
{customTokenData.name}
|
|
591
586
|
</div>
|
|
592
587
|
<div className="text-xs text-t_text_primary !text-opacity-50 font-medium">
|
|
593
|
-
{
|
|
588
|
+
{importChain?.displayName}
|
|
594
589
|
</div>
|
|
595
590
|
</div>
|
|
596
|
-
<div className="
|
|
591
|
+
<div className="text-sm text-t_text_primary text-opacity-50">
|
|
597
592
|
{customTokenData.symbol}
|
|
598
593
|
</div>
|
|
599
594
|
</div>
|
|
600
595
|
<div>
|
|
601
596
|
<Button
|
|
602
597
|
type="button"
|
|
603
|
-
className="h-8 py-0 px-4
|
|
598
|
+
className="h-8 py-0 px-4 leading-[14px] rounded-lg bg-gradient-to-br from-t_main_accent_light to-t_main_accent_dark !text-t_button_pr_text"
|
|
604
599
|
onClick={() => setShowImportWarning(true)}
|
|
605
600
|
>
|
|
606
601
|
Import
|
|
@@ -608,83 +603,38 @@ export const TokenPicker = ({
|
|
|
608
603
|
</div>
|
|
609
604
|
</div>
|
|
610
605
|
) : (
|
|
611
|
-
<div className="flex flex-col h-full
|
|
606
|
+
<div className="flex flex-col h-full">
|
|
612
607
|
<GroupedVirtuoso
|
|
613
608
|
style={{ maxHeight: "700px", height: "100%" }}
|
|
614
|
-
groupCounts={tokenGroups.map((group) => group
|
|
615
|
-
scrollerRef={(ref) => {
|
|
616
|
-
if (ref) {
|
|
617
|
-
ref.addEventListener("scroll", (e) => {
|
|
618
|
-
const target = e.target as HTMLDivElement;
|
|
619
|
-
setShowQuickPicks(target.scrollTop < 10);
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
}}
|
|
609
|
+
groupCounts={tokenGroups.map((group) => group.tokens.length)}
|
|
623
610
|
groupContent={(index) => (
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
: `${chain.displayName} network`;
|
|
637
|
-
}
|
|
638
|
-
case 1: {
|
|
639
|
-
if (!address && !solanaAddress) {
|
|
640
|
-
return "Other networks";
|
|
641
|
-
}
|
|
642
|
-
return `${chain.displayName} network`;
|
|
643
|
-
}
|
|
644
|
-
case 2: {
|
|
645
|
-
return "Other networks";
|
|
646
|
-
}
|
|
647
|
-
default: {
|
|
648
|
-
return "Undefined group";
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
} else {
|
|
652
|
-
// chain is not selected
|
|
653
|
-
switch (index) {
|
|
654
|
-
case 0: {
|
|
655
|
-
if (!address) {
|
|
656
|
-
return "All networks";
|
|
657
|
-
}
|
|
658
|
-
return ownedTokens.length
|
|
659
|
-
? "My tokens"
|
|
660
|
-
: "All networks";
|
|
661
|
-
}
|
|
662
|
-
case 1: {
|
|
663
|
-
return "All networks";
|
|
664
|
-
}
|
|
665
|
-
default: {
|
|
666
|
-
return "Undefined group";
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
})()}
|
|
671
|
-
</div>
|
|
672
|
-
<div className="horizontal-separator" />
|
|
673
|
-
{!tokenGroups[index]?.length && (
|
|
674
|
-
<div className="text-center py-1">No token found</div>
|
|
611
|
+
// Sticky section label: a near-opaque, blurred, widget-glass-colored
|
|
612
|
+
// band — rows dissolve under it while scrolling instead
|
|
613
|
+
// of showing through, and the band still picks up a hint
|
|
614
|
+
// of the page glow like the glass around it.
|
|
615
|
+
<div className="backdrop-blur-xl bg-gradient-to-r from-t_bg_secondary/40 to-t_bg_secondary/40 bg-t_bg_primary bg-opacity-75">
|
|
616
|
+
<p className="pb-3 text-[10px] font-medium leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
|
|
617
|
+
{tokenGroups[index]?.label}
|
|
618
|
+
</p>
|
|
619
|
+
{!tokenGroups[index]?.tokens.length && (
|
|
620
|
+
<div className="text-center py-1 pb-3 text-sm text-t_text_primary text-opacity-50">
|
|
621
|
+
No token found
|
|
622
|
+
</div>
|
|
675
623
|
)}
|
|
676
624
|
</div>
|
|
677
625
|
)}
|
|
678
626
|
itemContent={(index) => {
|
|
627
|
+
const token = allTokens[index];
|
|
679
628
|
return (
|
|
680
|
-
|
|
629
|
+
token && (
|
|
681
630
|
<TokenItem
|
|
682
631
|
panelType={type}
|
|
683
|
-
token={
|
|
632
|
+
token={token}
|
|
684
633
|
selectedChainId={chain?.chainId}
|
|
685
634
|
selectedTokenAddress={selectedTokenAddress}
|
|
635
|
+
resetWarning={getResetWarning(token)}
|
|
686
636
|
onClick={() => {
|
|
687
|
-
onSelect(
|
|
637
|
+
onSelect(token);
|
|
688
638
|
setModalOpen(false);
|
|
689
639
|
}}
|
|
690
640
|
/>
|