@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
@@ -1,4 +1,4 @@
1
- import { ArrowDownIcon, TokensIcon } from "@src/assets/icons";
1
+ import { CaretDownIcon } from "@src/assets/icons";
2
2
  import { Skeleton } from "@src/components";
3
3
  import { useSwapContext } from "@src/context";
4
4
  import { safeBigNumberFrom, weiToHumanReadable } from "@src/utils";
@@ -176,38 +176,37 @@ export const FeesPanel = () => {
176
176
  isFetchingSolanaFee);
177
177
 
178
178
  return (
179
- <div className="flex flex-col x-border rounded-xl py-2 px-4 bg-t_bg_tertiary bg-opacity-5">
179
+ <div className="flex flex-col rounded-xl py-2 px-4 sm:px-6 bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/0">
180
180
  <button
181
181
  aria-label={`${expanded ? "Collapse" : "Expand"} fees panel`}
182
182
  type="button"
183
183
  onClick={() => setExpanded((e) => !e)}
184
184
  >
185
- <div className="flex justify-between">
186
- <div className="flex gap-2 items-center text-xs font-medium sm:text-sm fill-t_text_primary">
187
- <TokensIcon />
188
- <p className="opacity-60">Fee: </p>
185
+ <div className="flex justify-between items-center py-1">
186
+ <div className="flex gap-2 items-center text-sm leading-6 tracking-[0.01em]">
187
+ <p className="text-t_text_primary text-opacity-50">Fee</p>
188
+ <div className="w-0.5 h-0.5 bg-t_text_primary opacity-25" />
189
189
  {isLoading ? (
190
- <Skeleton className="w-[100px] h-full" />
190
+ <Skeleton className="w-[100px] h-[20px]" />
191
191
  ) : (
192
- <p>{`${solanaBridgeFee ? solanaBridgeFee : totalFee} ${
193
- feeToken?.symbol || ""
194
- }`}</p>
192
+ <p className="text-t_text_primary text-opacity-80">{`${
193
+ solanaBridgeFee ? solanaBridgeFee : totalFee
194
+ } ${feeToken?.symbol || ""}`}</p>
195
195
  )}
196
- </div>
197
- <div className="flex gap-2 items-center fill-t_text_primary">
198
196
  {route && !isLoading && srcChain?.chainId !== dstChain?.chainId && (
199
- <DeliveryInfo />
200
- )}
201
- {expanded ? (
202
- <div className="w-4 h-2 rotate-180">
203
- <ArrowDownIcon />
204
- </div>
205
- ) : (
206
- <div className="w-4 h-2">
207
- <ArrowDownIcon />
208
- </div>
197
+ <>
198
+ <span className="text-t_text_primary opacity-50">/</span>
199
+ <DeliveryInfo />
200
+ </>
209
201
  )}
210
202
  </div>
203
+ <div
204
+ className={`flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-75 transition-transform [&_svg]:w-2.5 [&_svg]:h-[5px] ${
205
+ expanded ? "rotate-180" : ""
206
+ }`}
207
+ >
208
+ <CaretDownIcon />
209
+ </div>
211
210
  </div>
212
211
  {expanded && <Fees solanaBridgeFee={solanaBridgeFee} />}
213
212
  </button>
@@ -60,6 +60,7 @@ export const SwapButton = () => {
60
60
  openSuiWalletModal,
61
61
  setOpenSuiWalletModal,
62
62
  solana,
63
+ sui,
63
64
  } = useWallet();
64
65
 
65
66
  const { allowance, approve } = useERC20Token({
@@ -121,7 +122,10 @@ export const SwapButton = () => {
121
122
  };
122
123
  }
123
124
 
124
- if (srcChain?.ecosystem !== Ecosystem.SOLANA && !address) {
125
+ if (
126
+ (!srcChain || srcChain.ecosystem === Ecosystem.EVM) &&
127
+ !address
128
+ ) {
125
129
  return {
126
130
  text:
127
131
  integrationConfig.defaultWalletPicker || hasOnConnectWalletCallback
@@ -193,7 +197,20 @@ export const SwapButton = () => {
193
197
  };
194
198
  }
195
199
  if (
196
- srcChain?.ecosystem !== Ecosystem.SOLANA &&
200
+ (srcChain?.ecosystem === Ecosystem.SUI ||
201
+ dstChain?.ecosystem === Ecosystem.SUI) &&
202
+ !sui.address
203
+ ) {
204
+ return {
205
+ text: "Connect SUI Wallet",
206
+ fn: () => {
207
+ setOpenSuiWalletModal(true);
208
+ },
209
+ disabled: false,
210
+ };
211
+ }
212
+ if (
213
+ srcChain?.ecosystem === Ecosystem.EVM &&
197
214
  chainId?.toString() !== srcChainId
198
215
  ) {
199
216
  return {
@@ -256,7 +273,7 @@ export const SwapButton = () => {
256
273
  }
257
274
 
258
275
  return {
259
- text: bridgeUI ? "Bridge" : "Swap",
276
+ text: bridgeUI ? "Bridge Now" : "SWAP Now",
260
277
  fn: async () => {
261
278
  showSwapConfirmationView();
262
279
  },
@@ -265,7 +282,7 @@ export const SwapButton = () => {
265
282
  }, [
266
283
  isAsyncDataLoaded,
267
284
  error,
268
- srcChain?.ecosystem,
285
+ srcChain,
269
286
  dstChain?.ecosystem,
270
287
  address,
271
288
  srcChainId,
@@ -273,6 +290,8 @@ export const SwapButton = () => {
273
290
  srcTokenAddress,
274
291
  dstTokenAddress,
275
292
  solana.address,
293
+ sui.address,
294
+ setOpenSuiWalletModal,
276
295
  chainId,
277
296
  srcValueWei,
278
297
  isFetchingRoute,
@@ -323,10 +342,10 @@ export const SwapButton = () => {
323
342
  type="button"
324
343
  disabled={button.disabled}
325
344
  onClick={button.fn}
326
- className={`text-xl font-sans-bold font-bold w-full py-5 cursor-pointer disabled:cursor-not-allowed rounded-2xl border-none ${
345
+ className={`text-base font-medium leading-6 tracking-[0.01em] w-full px-6 py-3 cursor-pointer disabled:cursor-not-allowed rounded-lg border-none transition-[filter] ${
327
346
  button.disabled
328
347
  ? "bg-t_button_pr_off_bg text-t_button_pr_off_text"
329
- : "bg-gradient-to-r from-t_main_accent_light to-t_main_accent_dark text-t_button_pr_text"
348
+ : "bg-gradient-to-b from-t_main_accent_light from-50% to-t_main_accent_dark text-t_button_pr_text hover:brightness-105"
330
349
  }`}
331
350
  >
332
351
  {button.text}
@@ -1,21 +1,65 @@
1
1
  import { Skeleton } from "@src/components";
2
2
  import { useSwapContext } from "@src/context";
3
+ import { weiToHumanReadable } from "@src/utils";
4
+ import { useMemo } from "react";
5
+ import { SwapPanelType } from "../../../../SwapView";
3
6
 
4
- export const Balance = () => {
5
- const { srcTokenBalanceInfo, isFetchingBalance } = useSwapContext();
7
+ type Props = {
8
+ type: SwapPanelType;
9
+ };
10
+
11
+ export const Balance = ({ type }: Props) => {
12
+ const {
13
+ srcToken,
14
+ srcTokenBalanceInfo,
15
+ isFetchingBalance,
16
+ isFetchingBalances,
17
+ dstToken,
18
+ dstChain,
19
+ dstChainTokensOptions,
20
+ } = useSwapContext();
21
+
22
+ const dstTokenBalanceInfo = useMemo(() => {
23
+ if (!dstToken || !dstChain) {
24
+ return "";
25
+ }
26
+ const tokenOption = dstChainTokensOptions.find(
27
+ (token) =>
28
+ token.address.toLowerCase() === dstToken.address.toLowerCase() &&
29
+ token.chainId === dstChain.chainId,
30
+ );
31
+ if (!tokenOption?.balance || tokenOption.balance.eq(0)) {
32
+ return "";
33
+ }
34
+ return weiToHumanReadable({
35
+ amount: tokenOption.balance.toString(),
36
+ decimals: dstToken.decimals,
37
+ precisionFractionalPlaces: 5,
38
+ });
39
+ }, [dstToken, dstChain, dstChainTokensOptions]);
40
+
41
+ const token = type === "source" ? srcToken : dstToken;
42
+ const balanceInfo =
43
+ type === "source" ? srcTokenBalanceInfo : dstTokenBalanceInfo;
44
+ const isLoading =
45
+ type === "source" ? isFetchingBalance : isFetchingBalances;
46
+
47
+ if (!token || (!balanceInfo && !isLoading)) {
48
+ return null;
49
+ }
6
50
 
7
51
  return (
8
- <div className="flex flex-col items-end">
9
- <p className="text-xs font-medium opacity-60">Balance:</p>
10
- {isFetchingBalance ? (
11
- <Skeleton className="w-[50px] h-[16px]" />
52
+ <div className="flex items-center justify-end py-1">
53
+ {isLoading ? (
54
+ <Skeleton className="w-[80px] h-[16px]" />
12
55
  ) : (
13
- <p
14
- className={`text-xs font-medium ${
15
- !srcTokenBalanceInfo && "opacity-60"
16
- }`}
17
- >
18
- {srcTokenBalanceInfo || "-"}
56
+ <p className="text-[10px] leading-4 text-right whitespace-nowrap">
57
+ <span className="text-t_text_primary text-opacity-50">
58
+ {`Balance. `}
59
+ </span>
60
+ <span className="text-t_text_primary text-opacity-75 tracking-[0.032em]">
61
+ {`${balanceInfo} ${token.symbol}`}
62
+ </span>
19
63
  </p>
20
64
  )}
21
65
  </div>
@@ -1,9 +1,9 @@
1
1
  import { SafeInput, Skeleton } from "@src/components";
2
2
  import { NUMBER_INPUT_REGEX } from "@src/constants";
3
3
  import { useSwapContext } from "@src/context";
4
+ import { weiToHumanReadable } from "@src/utils";
4
5
  import { useMemo } from "react";
5
6
  import { SwapPanelType } from "../../../SwapView";
6
- import { Balance } from "./Balance";
7
7
  import { Ecosystem } from "@src/models";
8
8
 
9
9
  type Props = {
@@ -14,16 +14,29 @@ export const AmountPanel = ({ type }: Props) => {
14
14
  const {
15
15
  srcChain,
16
16
  srcValue,
17
- srcValueUsd,
17
+ srcToken,
18
+ srcTokenBalanceWei,
18
19
  dstToken,
19
20
  dstChain,
20
21
  dstValue,
21
- dstValueUsd,
22
22
  setSrcValue,
23
23
  isFetchingRoute,
24
24
  bridgeUI,
25
25
  } = useSwapContext();
26
26
 
27
+ const onMaxClick = () => {
28
+ if (!srcTokenBalanceWei) {
29
+ return;
30
+ }
31
+ setSrcValue(
32
+ weiToHumanReadable({
33
+ amount: srcTokenBalanceWei,
34
+ decimals: srcToken?.decimals || 18,
35
+ precisionFractionalPlaces: srcToken?.decimals || 18,
36
+ }),
37
+ );
38
+ };
39
+
27
40
  const value = useMemo(() => {
28
41
  if (
29
42
  type === "destination" &&
@@ -45,46 +58,34 @@ export const AmountPanel = ({ type }: Props) => {
45
58
  dstChain,
46
59
  ]);
47
60
 
48
- const valueUsd = useMemo(
49
- () => (type === "source" ? srcValueUsd : dstValueUsd),
50
- [dstValueUsd, srcValueUsd, type],
51
- );
52
-
53
61
  return (
54
- <div className="flex justify-between items-center w-full p-4 gap-8 overflow-hidden">
55
- <div className="flex flex-col font-medium w-full overflow-hidden text-left">
56
- {type === "source" ? (
57
- <>
58
- <SafeInput
59
- id="swap-amount-input"
60
- className={`bg-transparent border-none p-0 text-xl outline-none font-sans-bold font-bold`}
61
- regex={NUMBER_INPUT_REGEX}
62
- value={srcValue}
63
- onChange={(value) => setSrcValue(value)}
64
- />
65
- {valueUsd && (
66
- <div className={`text-sx opacity-60`}>{`$${valueUsd}`}</div>
67
- )}
68
- </>
69
- ) : isFetchingRoute ? (
70
- <div className="flex flex-col gap-1">
71
- <Skeleton className="w-[150px] h-[30px]" />
72
- <Skeleton className="w-[100px] h-[20px]" />
73
- </div>
74
- ) : (
75
- <>
76
- <div
77
- className={`text-xl text-t_text_secondary font-sans-bold font-bold`}
62
+ <div className="flex items-center gap-3 flex-1 min-w-0 overflow-hidden text-left">
63
+ {type === "source" ? (
64
+ <>
65
+ <SafeInput
66
+ id="swap-amount-input"
67
+ className="bg-transparent border-none p-0 text-2xl leading-8 outline-none w-full min-w-0 text-t_text_primary placeholder:text-t_text_primary placeholder:text-opacity-40"
68
+ regex={NUMBER_INPUT_REGEX}
69
+ value={srcValue}
70
+ onChange={(value) => setSrcValue(value)}
71
+ />
72
+ {!!srcTokenBalanceWei && (
73
+ <button
74
+ type="button"
75
+ onClick={onMaxClick}
76
+ className="shrink-0 px-2.5 py-1 rounded-full border border-solid border-t_main_accent_light bg-gradient-to-r from-t_main_accent_light/25 to-t_main_accent_dark/25 text-[10px] font-medium leading-[18px] tracking-[0.08em] text-t_text_primary hover:from-t_main_accent_light/40 hover:to-t_main_accent_dark/40 transition-colors"
78
77
  >
79
- {value || 0}
80
- </div>
81
- {valueUsd && (
82
- <div className={`text-sx opacity-60`}>{`$${valueUsd}`}</div>
83
- )}
84
- </>
85
- )}
86
- </div>
87
- {type === "source" && <Balance />}
78
+ MAX
79
+ </button>
80
+ )}
81
+ </>
82
+ ) : isFetchingRoute ? (
83
+ <Skeleton className="w-[150px] h-[32px]" />
84
+ ) : (
85
+ <div className="text-2xl leading-8 text-t_text_primary opacity-60 truncate">
86
+ {value || 0}
87
+ </div>
88
+ )}
88
89
  </div>
89
90
  );
90
91
  };
@@ -0,0 +1,143 @@
1
+ import { CloseIcon, SearchIcon, WarningIcon } from "@src/assets/icons";
2
+ import { Chain } from "@src/models";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { Virtuoso } from "react-virtuoso";
5
+
6
+ type ChainOption = Chain & {
7
+ disabled?: boolean;
8
+ };
9
+
10
+ type Props = {
11
+ chains: ChainOption[];
12
+ selectedChainId?: string;
13
+ /**
14
+ * Chain occupied by the other side of the bridge — picking it requires
15
+ * resetting the source, same as picking an unreachable chain.
16
+ */
17
+ sameChainId?: string;
18
+ onSelect: (chain: ChainOption) => void;
19
+ onClose: () => void;
20
+ };
21
+
22
+ /**
23
+ * Network picker used in bridge mode, where the destination token is
24
+ * derived from the source token and only the chain gets picked by the user.
25
+ */
26
+ export const ChainPicker = ({
27
+ chains,
28
+ selectedChainId,
29
+ sameChainId,
30
+ onSelect,
31
+ onClose,
32
+ }: Props) => {
33
+ const [searchValue, setSearchValue] = useState("");
34
+ const chainSearchRef = useRef<HTMLInputElement | null>(null);
35
+
36
+ useEffect(() => {
37
+ setTimeout(() => {
38
+ chainSearchRef.current?.focus();
39
+ }, 10); // delay focus a tiny bit to ensure it works
40
+ }, []);
41
+
42
+ const filteredChains = useMemo(() => {
43
+ if (!searchValue) return chains;
44
+ const searchLower = searchValue.toLowerCase();
45
+ return chains.filter(
46
+ (chain) =>
47
+ chain.displayName.toLowerCase().includes(searchLower) ||
48
+ chain.chainId.toLowerCase().includes(searchLower),
49
+ );
50
+ }, [chains, searchValue]);
51
+
52
+ // The current source cannot reach this chain — picking it resets the source.
53
+ const requiresSourceReset = (chain: ChainOption) =>
54
+ !!chain.disabled || chain.chainId === sameChainId;
55
+
56
+ return (
57
+ <>
58
+ <div className="flex justify-between items-center mb-6">
59
+ <div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
60
+ Select Network
61
+ </div>
62
+ <button
63
+ type="button"
64
+ aria-label="Close"
65
+ 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"
66
+ onClick={onClose}
67
+ >
68
+ <div className="w-4 h-4 opacity-50 fill-t_text_primary flex items-center justify-center">
69
+ <CloseIcon />
70
+ </div>
71
+ </button>
72
+ </div>
73
+
74
+ <div className="flex items-center gap-2 h-14 px-4 rounded-xl bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-5 mb-6">
75
+ <div className="fill-t_text_primary opacity-50 [&_svg]:w-4 [&_svg]:h-4 flex items-center shrink-0">
76
+ <SearchIcon />
77
+ </div>
78
+ <input
79
+ ref={chainSearchRef}
80
+ value={searchValue}
81
+ onChange={(e) => setSearchValue(e.target.value)}
82
+ placeholder="Search network name"
83
+ 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"
84
+ />
85
+ </div>
86
+
87
+ <div className="flex flex-col mx-0 h-full min-h-0 flex-1">
88
+ {filteredChains.length ? (
89
+ <Virtuoso
90
+ style={{ maxHeight: "700px", height: "100%" }}
91
+ data={filteredChains}
92
+ itemContent={(_, chain) => {
93
+ const resetWarning = requiresSourceReset(chain);
94
+ const isSelected = chain.chainId === selectedChainId;
95
+ return (
96
+ <div className="pb-2">
97
+ <div
98
+ onClick={() => onSelect(chain)}
99
+ className={`flex gap-3 items-center p-3 sm:p-4 rounded-xl cursor-pointer bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/[0.02] bg-[rgba(40,40,40,0.15)] hover:bg-t_bg_tertiary hover:bg-opacity-10 transition-colors ${
100
+ isSelected ? "!bg-t_bg_tertiary !bg-opacity-10" : ""
101
+ }`}
102
+ >
103
+ <img
104
+ src={chain.image}
105
+ alt={chain.displayName}
106
+ className={`w-10 h-10 rounded-full shrink-0 ${
107
+ resetWarning ? "opacity-40" : ""
108
+ }`}
109
+ />
110
+ <div className="flex justify-between items-center gap-2 w-full min-w-0">
111
+ <p
112
+ className={`text-sm font-medium leading-5 tracking-[0.01em] text-t_text_primary ${
113
+ resetWarning ? "opacity-40" : ""
114
+ }`}
115
+ >
116
+ {chain.displayName}
117
+ </p>
118
+ {resetWarning && (
119
+ <div className="flex items-center justify-end gap-1.5 text-[#D77F02]">
120
+ <WarningIcon className="w-3.5 h-3.5 shrink-0" />
121
+ <span className="text-xs leading-[18px] tracking-[0.01em] whitespace-nowrap">
122
+ <span className="sm:hidden">Source will reset.</span>
123
+ <span className="hidden sm:inline">
124
+ Source will reset after selection.
125
+ </span>
126
+ </span>
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ </div>
132
+ );
133
+ }}
134
+ />
135
+ ) : (
136
+ <div className="text-center py-1 text-sm text-t_text_primary text-opacity-50">
137
+ No network found
138
+ </div>
139
+ )}
140
+ </div>
141
+ </>
142
+ );
143
+ };
@@ -0,0 +1,159 @@
1
+ import { ChevronDownIcon, SearchIcon } from "@src/assets/icons";
2
+ import { useSwapContext } from "@src/context";
3
+ import { SwapPanelType } from "@src/components/Swap/SwapView";
4
+ import { Chain } from "@src/models";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { Virtuoso } from "react-virtuoso";
7
+
8
+ type Props = {
9
+ type: SwapPanelType;
10
+ selectedChain: Chain | undefined;
11
+ onSelect: (chain: Chain | undefined) => void;
12
+ };
13
+
14
+ /**
15
+ * Network filter embedded in the token picker search bar.
16
+ * Selecting a network only narrows the token list — the swap chain is
17
+ * set when a token gets picked.
18
+ */
19
+ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
20
+ const { srcChainOptions, dstChainOptions } = useSwapContext();
21
+ const [open, setOpen] = useState(false);
22
+ const [searchValue, setSearchValue] = useState("");
23
+ const containerRef = useRef<HTMLDivElement | null>(null);
24
+
25
+ const chains = useMemo(
26
+ () => (type === "source" ? srcChainOptions : dstChainOptions),
27
+ [type, srcChainOptions, dstChainOptions],
28
+ );
29
+
30
+ const filteredChains = useMemo(() => {
31
+ if (!searchValue) return chains;
32
+ const searchLower = searchValue.toLowerCase();
33
+ return chains.filter(
34
+ (chain) =>
35
+ chain.displayName.toLowerCase().includes(searchLower) ||
36
+ chain.chainId.toLowerCase().includes(searchLower),
37
+ );
38
+ }, [chains, searchValue]);
39
+
40
+ useEffect(() => {
41
+ if (!open) {
42
+ setSearchValue("");
43
+ }
44
+ }, [open]);
45
+
46
+ useEffect(() => {
47
+ const onPointerDown = (e: Event) => {
48
+ if (
49
+ containerRef.current &&
50
+ !e.composedPath().includes(containerRef.current)
51
+ ) {
52
+ setOpen(false);
53
+ }
54
+ };
55
+ document.addEventListener("mousedown", onPointerDown);
56
+ return () => document.removeEventListener("mousedown", onPointerDown);
57
+ }, []);
58
+
59
+ const previewChains = chains.slice(0, 3);
60
+
61
+ return (
62
+ <div className="relative" ref={containerRef}>
63
+ <button
64
+ type="button"
65
+ onClick={() => setOpen((state) => !state)}
66
+ className="flex items-center gap-2 h-12 px-3 py-2 rounded-lg bg-t_bg_tertiary bg-opacity-[0.04] hover:bg-opacity-10 transition-colors"
67
+ >
68
+ {selectedChain ? (
69
+ <img
70
+ src={selectedChain.image}
71
+ alt={selectedChain.displayName}
72
+ className="w-5 h-5 rounded-full"
73
+ />
74
+ ) : (
75
+ <div className="flex -space-x-1.5 shrink-0">
76
+ {previewChains.map((chain) => (
77
+ <img
78
+ key={chain.chainId}
79
+ src={chain.image}
80
+ alt={chain.displayName}
81
+ className="w-5 h-5 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
82
+ />
83
+ ))}
84
+ </div>
85
+ )}
86
+ <p className="text-sm leading-6 tracking-[0.01em] text-t_text_primary whitespace-nowrap max-w-[120px] truncate">
87
+ {selectedChain
88
+ ? selectedChain.displayName
89
+ : `${chains.length} Networks`}
90
+ </p>
91
+ <div
92
+ className={`flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 transition-transform [&_svg]:w-3 [&_svg]:h-3 ${
93
+ open ? "rotate-180" : ""
94
+ }`}
95
+ >
96
+ <ChevronDownIcon />
97
+ </div>
98
+ </button>
99
+
100
+ {open && (
101
+ <div className="absolute right-0 top-[calc(100%+8px)] z-20 flex flex-col w-[220px] max-h-[280px] rounded-xl bg-t_bg_primary border border-solid border-t_text_primary border-opacity-10 backdrop-blur-[24px] overflow-hidden shadow-lg">
102
+ <div className="flex items-center gap-2 px-4 py-3 border-b border-solid border-t_text_primary border-opacity-10">
103
+ <div className="fill-t_text_primary opacity-50 [&_svg]:w-3.5 [&_svg]:h-3.5 flex items-center">
104
+ <SearchIcon />
105
+ </div>
106
+ <input
107
+ value={searchValue}
108
+ onChange={(e) => setSearchValue(e.target.value)}
109
+ placeholder={`Browse ${chains.length} Chains...`}
110
+ className="bg-transparent border-none outline-none w-full text-sm text-t_text_primary placeholder:text-t_text_primary placeholder:text-opacity-40"
111
+ />
112
+ </div>
113
+ <div className="h-[200px]">
114
+ <Virtuoso
115
+ style={{ height: "200px" }}
116
+ data={[undefined, ...filteredChains] as (Chain | undefined)[]}
117
+ itemContent={(_, chain) => (
118
+ <button
119
+ type="button"
120
+ onClick={() => {
121
+ onSelect(chain);
122
+ setOpen(false);
123
+ }}
124
+ className={`flex items-center gap-2 w-full px-4 py-2 text-left hover:bg-t_bg_tertiary hover:bg-opacity-10 transition-colors ${
125
+ (chain?.chainId || "") === (selectedChain?.chainId || "")
126
+ ? "bg-t_bg_tertiary bg-opacity-10"
127
+ : ""
128
+ }`}
129
+ >
130
+ {chain ? (
131
+ <img
132
+ src={chain.image}
133
+ alt={chain.displayName}
134
+ className="w-5 h-5 rounded-full"
135
+ />
136
+ ) : (
137
+ <div className="flex -space-x-1 shrink-0">
138
+ {previewChains.map((previewChain) => (
139
+ <img
140
+ key={previewChain.chainId}
141
+ src={previewChain.image}
142
+ alt={previewChain.displayName}
143
+ className="w-4 h-4 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
144
+ />
145
+ ))}
146
+ </div>
147
+ )}
148
+ <p className="text-sm leading-6 text-t_text_primary truncate">
149
+ {chain ? chain.displayName : "All Networks"}
150
+ </p>
151
+ </button>
152
+ )}
153
+ />
154
+ </div>
155
+ </div>
156
+ )}
157
+ </div>
158
+ );
159
+ };