@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.d.mts +10 -2
  3. package/dist/index.d.ts +10 -2
  4. package/dist/index.global.js +87 -86
  5. package/dist/index.js +2888 -2521
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +2828 -2458
  8. package/dist/index.mjs.map +1 -1
  9. package/localTheme.ts +5 -3
  10. package/package.json +1 -1
  11. package/src/assets/icons/AdjustmentsIcon.tsx +16 -0
  12. package/src/assets/icons/CaretDownIcon.tsx +12 -0
  13. package/src/assets/icons/CaretsUpDownIcon.tsx +13 -0
  14. package/src/assets/icons/ChainlinkMarkIcon.tsx +17 -0
  15. package/src/assets/icons/ClockIcon.tsx +14 -0
  16. package/src/assets/icons/CloseIcon.tsx +6 -2
  17. package/src/assets/icons/WarningIcon.tsx +19 -0
  18. package/src/assets/icons/index.ts +6 -0
  19. package/src/components/Modal/index.tsx +14 -9
  20. package/src/components/PoweredBy/index.tsx +12 -5
  21. package/src/components/Swap/Header/Controls/index.tsx +50 -36
  22. package/src/components/Swap/Header/index.tsx +12 -5
  23. package/src/components/Swap/HistoryView/ActivityCard/index.tsx +212 -0
  24. package/src/components/Swap/HistoryView/index.tsx +24 -6
  25. package/src/components/Swap/PickerView/index.tsx +151 -0
  26. package/src/components/Swap/ReorderButton/ReorderButton.tsx +5 -7
  27. package/src/components/Swap/SettingsView/Delivery/index.tsx +27 -28
  28. package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +27 -24
  29. package/src/components/Swap/SettingsView/Slippage/index.tsx +44 -43
  30. package/src/components/Swap/SettingsView/index.tsx +1 -1
  31. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +50 -4
  32. package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +9 -19
  33. package/src/components/Swap/SwapView/FeesPanel/Fee/index.tsx +4 -4
  34. package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +5 -9
  35. package/src/components/Swap/SwapView/FeesPanel/index.tsx +21 -22
  36. package/src/components/Swap/SwapView/SwapButton/index.tsx +25 -6
  37. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +56 -12
  38. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +42 -41
  39. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
  40. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +159 -0
  41. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +105 -55
  42. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +226 -276
  43. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +57 -97
  44. package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +27 -0
  45. package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +60 -49
  46. package/src/components/Swap/SwapView/SwapPanel/index.tsx +20 -25
  47. package/src/components/Swap/SwapView/WalletPicker/index.tsx +6 -1
  48. package/src/components/Swap/SwapView/index.tsx +2 -1
  49. package/src/components/Swap/index.tsx +20 -4
  50. package/src/components/ToggleButton/index.tsx +18 -15
  51. package/src/components/TxConfigForm/index.tsx +5 -5
  52. package/src/components/TxWidgetWC/index.tsx +4 -0
  53. package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
  54. package/src/context/HistoryProvider.tsx +2 -1
  55. package/src/context/SwapProvider.tsx +87 -62
  56. package/src/context/TxUIWrapper.tsx +3 -3
  57. package/src/hooks/useEvmContractApi.ts +28 -4
  58. package/src/models/TransactionHistory.ts +1 -0
  59. package/src/models/payloads/GetBalancesPayload.ts +1 -0
  60. package/src/models/payloads/ModalIntegrationPayload.ts +1 -0
  61. package/src/models/payloads/WidgetIntegrationPayload.ts +1 -0
  62. package/src/services/api.ts +37 -0
  63. package/src/utils/index.ts +48 -1
  64. package/src/utils/tokens.ts +20 -1
  65. package/tailwind.config.js +4 -1
  66. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +0 -97
  67. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/index.tsx +0 -103
  68. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/index.tsx +0 -65
  69. package/src/components/Swap/SwapView/SwapPanel/MaxPanel/index.tsx +0 -43
  70. 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 { QuickPickTokenItem } from "./QuickPickTokenItem";
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 [showQuickPicks, setShowQuickPicks] = useState(true);
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
- chain &&
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: chain.chainId,
109
+ chainId: importChain.chainId,
103
110
  };
104
111
 
105
112
  if (!isServer) {
106
- customTokens[chain.chainId] = {
107
- ...(customTokens[chain.chainId] || {}),
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
- chain,
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
- const responses = await Promise.all([
140
- evmContractApi.query(customTokenContract, "decimals"),
141
- evmContractApi.query(customTokenContract, "name"),
142
- evmContractApi.query(customTokenContract, "symbol"),
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
- const [decimals, name, symbol] = responses;
148
- setCustomTokenData({
149
- address: searchValue.toLowerCase(),
150
- decimals,
151
- name,
152
- symbol,
153
- });
148
+ setCustomTokenData(tokenData);
154
149
  } catch (_) {
155
- setCustomTokenData(null);
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
- }, [chain, searchValue, evmContractApi, supportedChains]);
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 === chain?.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
- }, [chain, searchValue, supportedChains]);
256
+ }, [importChain, searchValue, supportedChains]);
260
257
 
261
258
  useEffect(() => {
262
259
  if (
263
260
  !bridgeUI &&
264
- chain &&
261
+ importChain &&
265
262
  !findTokenDataByAddressAndChain(
266
- chain.ecosystem === Ecosystem.EVM
263
+ importChain.ecosystem === Ecosystem.EVM
267
264
  ? searchValue.toLowerCase()
268
265
  : searchValue,
269
- chain.chainId,
266
+ importChain.chainId,
270
267
  )
271
268
  ) {
272
- if (chain.ecosystem === Ecosystem.EVM && isETHAddressValid(searchValue)) {
269
+ if (
270
+ importChain.ecosystem === Ecosystem.EVM &&
271
+ isETHAddressValid(searchValue)
272
+ ) {
273
273
  getCustomTokenData();
274
274
  } else if (
275
- chain.ecosystem === Ecosystem.SOLANA &&
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
- chain,
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 filteredTokens = useMemo(
304
- () =>
305
- tokens?.filter(
306
- (token) =>
307
- isMatch(token.symbol, searchValue) ||
308
- isMatch(token.name, searchValue) ||
309
- token.address.toLowerCase() === searchValue.toLowerCase(),
310
- ),
311
- [searchValue, tokens],
312
- );
313
-
314
- const otherFilteredTokens = useMemo(
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 ownedFilteredTokens = useMemo(() => {
346
- return [
347
- ...filteredTokens.filter((token) =>
348
- BigNumber.from(token?.balance || "0").gt(0),
349
- ),
350
- ...otherFilteredTokens.filter((token) =>
351
- BigNumber.from(token?.balance || "0").gt(0),
352
- ),
353
- ].sort((tokenA, tokenB) => {
354
- const valueA = calculateTokenValue(tokenA);
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
- // If USD values are equal (or both 0), sort by priority
361
- return tokenB.priority - tokenA.priority;
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
- // Create groups based on chain and wallet connection status
378
- const groups = chain
379
- ? [
380
- ...(!!(address || solanaAddress) && ownedFilteredTokens.length
381
- ? [ownedFilteredTokens]
382
- : []),
383
- currentChainTokens,
384
- otherChainTokens,
385
- ]
386
- : [
387
- ...(!!(address || solanaAddress) && ownedFilteredTokens.length
388
- ? [ownedFilteredTokens]
389
- : []),
390
- [...currentChainTokens, ...otherChainTokens],
391
- ];
392
-
393
- return groups.flat();
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
- filteredTokens,
396
- otherFilteredTokens,
397
- ownedFilteredTokens,
398
- chain,
393
+ visibleTokens,
399
394
  address,
400
395
  solanaAddress,
396
+ calculateTokenValue,
397
+ compareByResetWarning,
401
398
  ]);
402
399
 
403
- const ownedTokens = useMemo(
404
- () => allTokens.filter((token) => token.balance?.gt(0)),
405
- [allTokens],
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
- const currentChainTokens = filteredTokens
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
- ...(!!(address || solanaAddress) && ownedFilteredTokens.length
423
- ? [ownedFilteredTokens]
424
- : []),
425
- currentChainTokens,
426
- otherChainTokens,
419
+ { label: "MY TOKENS.", tokens: ownedTokens },
420
+ { label: "RECOMMENDED.", tokens: recommendedTokens },
427
421
  ]
428
- : [
429
- ...(!!(address || solanaAddress) && ownedFilteredTokens.length
430
- ? [ownedFilteredTokens]
431
- : []),
432
- [...currentChainTokens, ...otherChainTokens],
433
- ];
434
- }, [
435
- chain,
436
- address,
437
- solanaAddress,
438
- filteredTokens,
439
- otherFilteredTokens,
440
- ownedFilteredTokens,
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-base/4 mb-4">Import token</div>
447
- <div
448
- className="w-4 h-4 fill-t_text_primary cursor-pointer"
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
- <CloseIcon />
452
- </div>
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={`token-picker-container grow-0 bg-t_bg_secondary border border-solid border-t_text_primary border-opacity-10 rounded-xl py-2 pr-2 pl-4 hover:cursor-pointer`}
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="token-picker__all__symbol">
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-white cursor-pointer hover:fill-t_main_accent_light"
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-2 py-4 rounded-xl border border-x_orange text-sm">
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-t_bg_secondary border border-solid border-t_text_primary border-opacity-10 rounded-xl p-2">
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-white rounded-md mr-2">
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-bold">Accept</div>
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={`token-picker__import-button w-full ${
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-base/4 text-t_text_primary mb-4">{`Pick ${type} token`}</div>
531
- <div
532
- className="w-4 h-4 fill-t_text_primary cursor-pointer"
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
- <CloseIcon />
536
- </div>
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
- <TextInput
540
- ref={tokenSearchRef}
541
- inputIcon={
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
- placeholder="Search name or paste address"
547
- value={searchValue}
548
- handleChange={setSearchValue}
549
- />
550
- <div
551
- className={`flex flex-wrap gap-1 my-4 mx-0 transition-all duration-300 ${
552
- showQuickPicks
553
- ? "opacity-100 max-h-[200px]"
554
- : "opacity-0 max-h-0 overflow-hidden"
555
- }`}
556
- >
557
- {tokens
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
- <div className="flex flex-col mx-0 h-full">
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={`token-picker-container hover:cursor-default`}>
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={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
- {chain?.displayName}
588
+ {importChain?.displayName}
594
589
  </div>
595
590
  </div>
596
- <div className="token-picker__all__symbol">
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 text-t_text_primary leading-[14px] rounded-[32px] bg-gradient-to-r from-t_main_accent_light to-t_main_accent_dark"
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 gap-8 w-[calc(100%+32px)] mx-[-16px]">
606
+ <div className="flex flex-col h-full">
612
607
  <GroupedVirtuoso
613
608
  style={{ maxHeight: "700px", height: "100%" }}
614
- groupCounts={tokenGroups.map((group) => group?.length)}
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
- <div className="py-1 bg-t_bg_primary text-t_text_primary">
625
- <div className="px-4 text-left">
626
- {(() => {
627
- if (chain) {
628
- // chain is selected
629
- switch (index) {
630
- case 0: {
631
- if (!address && !solanaAddress) {
632
- return `${chain.displayName} network`;
633
- }
634
- return ownedTokens.length
635
- ? "My tokens"
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
- allTokens[index] && (
629
+ token && (
681
630
  <TokenItem
682
631
  panelType={type}
683
- token={allTokens[index]}
632
+ token={token}
684
633
  selectedChainId={chain?.chainId}
685
634
  selectedTokenAddress={selectedTokenAddress}
635
+ resetWarning={getResetWarning(token)}
686
636
  onClick={() => {
687
- onSelect(allTokens[index]!);
637
+ onSelect(token);
688
638
  setModalOpen(false);
689
639
  }}
690
640
  />