@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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-
|
|
187
|
-
<
|
|
188
|
-
<
|
|
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-
|
|
190
|
+
<Skeleton className="w-[100px] h-[20px]" />
|
|
191
191
|
) : (
|
|
192
|
-
<p>{`${
|
|
193
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 (
|
|
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
|
|
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" : "
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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=
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
};
|