@xswap-link/sdk 0.14.0 → 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 +15 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.global.js +86 -86
- package/dist/index.js +2129 -1857
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2225 -1953
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +5 -3
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +5 -7
- package/src/assets/icons/ClockIcon.tsx +3 -7
- package/src/assets/icons/CloseIcon.tsx +6 -2
- package/src/assets/icons/WarningIcon.tsx +8 -5
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +6 -8
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/SettingsView/Delivery/index.tsx +1 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +50 -4
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +1 -1
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +1 -1
- package/src/components/Swap/SwapView/SwapButton/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +8 -6
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +11 -4
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +102 -73
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +53 -119
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +1 -1
- package/src/components/Swap/index.tsx +18 -4
- package/src/components/TxConfigForm/index.tsx +2 -2
- package/src/context/HistoryProvider.tsx +1 -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/payloads/GetBalancesPayload.ts +1 -0
- package/src/services/api.ts +37 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/tokens.ts +20 -1
- package/tailwind.config.js +4 -1
package/localTheme.ts
CHANGED
|
@@ -50,8 +50,9 @@ const lightThemeStyles: ModalIntegrationThemeStyles = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const darkThemeStyles: ModalIntegrationThemeStyles = {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// Lavender CTA gradient from the swap-v2 design
|
|
54
|
+
mainAccentLight: "#D3DAF2",
|
|
55
|
+
mainAccentDark: "#B8C1E9",
|
|
55
56
|
bgPrimary: "#020202",
|
|
56
57
|
bgSecondary: "#0F0F0F",
|
|
57
58
|
bgTertiary: "#ffffff",
|
|
@@ -62,7 +63,8 @@ const darkThemeStyles: ModalIntegrationThemeStyles = {
|
|
|
62
63
|
textGreen: "#4CAF50",
|
|
63
64
|
clickSecondary: "#3681C6",
|
|
64
65
|
clickTertiary: "#FFFFFF",
|
|
65
|
-
|
|
66
|
+
// Dark text sits on the light lavender primary button
|
|
67
|
+
buttonPrText: "#020202",
|
|
66
68
|
buttonPrOffBg: "#272727",
|
|
67
69
|
buttonPrOffText: "#90A4AE",
|
|
68
70
|
labelText: "#FFFFFF",
|
package/package.json
CHANGED
|
@@ -3,16 +3,14 @@ export const AdjustmentsIcon = ({ className }: { className?: string }) => {
|
|
|
3
3
|
<svg
|
|
4
4
|
xmlns="http://www.w3.org/2000/svg"
|
|
5
5
|
fill="none"
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// The 20px-wide artwork is clipped to a 16px box in the design,
|
|
7
|
+
// so the slider lines run edge to edge.
|
|
8
|
+
viewBox="2 0 16 10.4"
|
|
8
9
|
stroke="currentColor"
|
|
10
|
+
strokeLinejoin="round"
|
|
9
11
|
className={className}
|
|
10
12
|
>
|
|
11
|
-
<path
|
|
12
|
-
strokeLinecap="round"
|
|
13
|
-
strokeLinejoin="round"
|
|
14
|
-
d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"
|
|
15
|
-
/>
|
|
13
|
+
<path d="M8.4 2.1L20 2.1M5.2 2.1C5.2 1.21634 5.91635 0.5 6.8 0.5C7.68366 0.5 8.4 1.21635 8.4 2.1C8.4 2.98366 7.68366 3.7 6.8 3.7C5.91635 3.7 5.2 2.98365 5.2 2.1ZM0 2.1H5.2M14.8 8.3L20 8.3M11.6 8.3C11.6 7.41634 12.3163 6.7 13.2 6.7C14.0837 6.7 14.8 7.41635 14.8 8.3C14.8 9.18366 14.0837 9.9 13.2 9.9C12.3163 9.9 11.6 9.18366 11.6 8.3ZM6.8 8.3H11.6" />
|
|
16
14
|
</svg>
|
|
17
15
|
);
|
|
18
16
|
};
|
|
@@ -3,16 +3,12 @@ export const ClockIcon = ({ className }: { className?: string }) => {
|
|
|
3
3
|
<svg
|
|
4
4
|
xmlns="http://www.w3.org/2000/svg"
|
|
5
5
|
fill="none"
|
|
6
|
-
viewBox="0 0
|
|
7
|
-
strokeWidth={1.5}
|
|
6
|
+
viewBox="0 0 16 16"
|
|
8
7
|
stroke="currentColor"
|
|
9
8
|
className={className}
|
|
10
9
|
>
|
|
11
|
-
<path
|
|
12
|
-
|
|
13
|
-
strokeLinejoin="round"
|
|
14
|
-
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
15
|
-
/>
|
|
10
|
+
<path d="M8 0.5C12.1421 0.5 15.5 3.85786 15.5 8C15.5 12.0748 12.2503 15.3875 8.20117 15.4941L4.35352 11.6465C3.67047 10.924 2.67065 9.92446 1.84375 9.10742C1.42752 8.69616 1.05392 8.32896 0.78418 8.06543C0.671658 7.9555 0.576752 7.86393 0.504883 7.79395C0.614072 3.7471 3.92679 0.5 8 0.5Z" />
|
|
11
|
+
<path d="M8 4V8H10.0011" />
|
|
16
12
|
</svg>
|
|
17
13
|
);
|
|
18
14
|
};
|
|
@@ -6,10 +6,14 @@ export const CloseIcon: FC<{ className?: string }> = ({ className }) => {
|
|
|
6
6
|
height="18"
|
|
7
7
|
width="18"
|
|
8
8
|
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
-
viewBox="0 0 10
|
|
9
|
+
viewBox="0 0 10 9"
|
|
10
|
+
fill="none"
|
|
11
|
+
stroke="currentColor"
|
|
10
12
|
className={className || ""}
|
|
11
13
|
>
|
|
12
|
-
|
|
14
|
+
{/* Hairline 1px stroke at any render size, as in the design */}
|
|
15
|
+
<path vectorEffect="non-scaling-stroke" d="M0 0.5H1L9 8.5H10" />
|
|
16
|
+
<path vectorEffect="non-scaling-stroke" d="M10 0.5H9L1 8.5H0" />
|
|
13
17
|
</svg>
|
|
14
18
|
);
|
|
15
19
|
};
|
|
@@ -2,14 +2,17 @@ export const WarningIcon = ({ className }: { className?: string }) => {
|
|
|
2
2
|
return (
|
|
3
3
|
<svg
|
|
4
4
|
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
viewBox="0 0
|
|
6
|
-
fill="
|
|
5
|
+
viewBox="0 0 14 14"
|
|
6
|
+
fill="none"
|
|
7
7
|
className={className}
|
|
8
8
|
>
|
|
9
9
|
<path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
d="M11 0C12.6568 6.60986e-05 14 1.34319 14 3V11C14 12.6568 12.6568 13.9999 11 14H3C1.34315 14 1.61073e-08 12.6569 0 11V3C1.28858e-07 1.34315 1.34315 0 3 0H11Z"
|
|
11
|
+
fill="#F46F28"
|
|
12
|
+
/>
|
|
13
|
+
<path
|
|
14
|
+
d="M7.66699 9.66699V11H6.33301V9.66699H7.66699ZM8 3V4.33301L7.66699 5.66699V8.33301H6.33301V5.66699L6 4.33301V3H8Z"
|
|
15
|
+
fill="#0B0B0B"
|
|
13
16
|
/>
|
|
14
17
|
</svg>
|
|
15
18
|
);
|
|
@@ -7,6 +7,11 @@ import { FC, ReactNode } from "react";
|
|
|
7
7
|
* - Modals needs to be inside all app's main contexts,
|
|
8
8
|
* - Modals' need to rerender on props change.
|
|
9
9
|
*
|
|
10
|
+
* Presentation follows the swap-v2 design:
|
|
11
|
+
* - sm and up: the content replaces the widget in place (fills the widget
|
|
12
|
+
* container with the same radius), or is centered when in overlay mode,
|
|
13
|
+
* - below sm: a bottom sheet pinned to the viewport with a drag handle.
|
|
14
|
+
*
|
|
10
15
|
* Usage:
|
|
11
16
|
* ```
|
|
12
17
|
* const [open, setOpen] = useState(false);
|
|
@@ -30,12 +35,14 @@ export const Modal: FC<{
|
|
|
30
35
|
return (
|
|
31
36
|
<div
|
|
32
37
|
className={`${
|
|
33
|
-
overlay
|
|
34
|
-
|
|
38
|
+
overlay
|
|
39
|
+
? "fixed bg-[rgba(0,0,0,0.8)]"
|
|
40
|
+
: "fixed sm:absolute bg-[rgba(2,2,2,0.8)] sm:bg-transparent"
|
|
41
|
+
} top-0 left-0 right-0 bottom-0 flex flex-col justify-end sm:justify-center items-center z-10 ${
|
|
35
42
|
className || ""
|
|
36
43
|
}`}
|
|
37
44
|
onMouseDown={(e) => {
|
|
38
|
-
if (e.target === e.currentTarget
|
|
45
|
+
if (e.target === e.currentTarget) {
|
|
39
46
|
e.stopPropagation();
|
|
40
47
|
e.nativeEvent.stopImmediatePropagation();
|
|
41
48
|
e.preventDefault();
|
|
@@ -44,13 +51,11 @@ export const Modal: FC<{
|
|
|
44
51
|
}}
|
|
45
52
|
>
|
|
46
53
|
<div
|
|
47
|
-
className=
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
maxWidth: "636px",
|
|
51
|
-
maxHeight: "100%",
|
|
52
|
-
}}
|
|
54
|
+
className={`flex flex-col w-full bg-gradient-to-r from-t_bg_secondary/50 to-t_bg_secondary/50 bg-t_bg_primary border-t sm:border border-solid border-t_text_primary border-opacity-10 rounded-t-[24px] rounded-b-none sm:rounded-[32px] p-4 pt-2.5 sm:p-8 h-[85vh] max-h-full overflow-hidden ${
|
|
55
|
+
overlay ? "sm:h-[600px] sm:max-w-[636px]" : "sm:h-full"
|
|
56
|
+
}`}
|
|
53
57
|
>
|
|
58
|
+
<div className="sm:hidden mx-auto mb-4 h-1 w-8 rounded-full bg-t_bg_tertiary bg-opacity-10 shrink-0" />
|
|
54
59
|
{children}
|
|
55
60
|
</div>
|
|
56
61
|
</div>
|
|
@@ -3,17 +3,15 @@ import { ChainlinkMarkIcon } from "@src/assets/icons";
|
|
|
3
3
|
export const PoweredBy = () => {
|
|
4
4
|
return (
|
|
5
5
|
<div className="flex justify-end items-center">
|
|
6
|
-
<div className="flex items-center gap-2
|
|
7
|
-
<div className="flex items-center [&_svg]:h-
|
|
6
|
+
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg bg-gradient-to-r from-t_bg_tertiary/0 to-t_bg_tertiary/5">
|
|
7
|
+
<div className="flex items-center [&_svg]:h-5 [&_svg]:w-5">
|
|
8
8
|
<ChainlinkMarkIcon />
|
|
9
9
|
</div>
|
|
10
|
-
<p className="text-
|
|
11
|
-
<span className="text-t_text_primary text-opacity-
|
|
12
|
-
{`
|
|
13
|
-
</span>
|
|
14
|
-
<span className="text-t_text_primary text-opacity-75">
|
|
15
|
-
Chainlink.
|
|
10
|
+
<p className="text-xs whitespace-nowrap leading-5 tracking-[0.015em]">
|
|
11
|
+
<span className="text-t_text_primary text-opacity-70">
|
|
12
|
+
{`POWERED BY `}
|
|
16
13
|
</span>
|
|
14
|
+
<span className="text-t_text_primary">CCIP.</span>
|
|
17
15
|
</p>
|
|
18
16
|
</div>
|
|
19
17
|
</div>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useSwapContext } from "@src/context";
|
|
2
|
+
import { Chain, TokenOption } from "@src/models";
|
|
3
|
+
import {
|
|
4
|
+
isBridgeTokenConnectionSupported,
|
|
5
|
+
isChainConnectionSupported,
|
|
6
|
+
} from "@src/utils";
|
|
7
|
+
import { ChainPicker } from "../SwapView/SwapPanel/TokenPanel/ChainPicker";
|
|
8
|
+
import { TokenPicker } from "../SwapView/SwapPanel/TokenPanel/TokenPicker";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Token/network picker rendered while `activePicker` is set:
|
|
12
|
+
* - sm and up it replaces the swap view inside the widget container, so it
|
|
13
|
+
* sits on the exact same glass background,
|
|
14
|
+
* - below sm the same element becomes a bottom sheet pinned to the viewport.
|
|
15
|
+
*/
|
|
16
|
+
export const PickerView = () => {
|
|
17
|
+
const {
|
|
18
|
+
activePicker,
|
|
19
|
+
setActivePicker,
|
|
20
|
+
bridgeUI,
|
|
21
|
+
srcChain,
|
|
22
|
+
dstChain,
|
|
23
|
+
srcToken,
|
|
24
|
+
dstToken,
|
|
25
|
+
srcChainTokensOptions,
|
|
26
|
+
srcChainOtherTokensOptions,
|
|
27
|
+
dstChainTokensOptions,
|
|
28
|
+
dstChainOtherTokensOptions,
|
|
29
|
+
setSrcToken,
|
|
30
|
+
setSrcChain,
|
|
31
|
+
setDstToken,
|
|
32
|
+
setDstChain,
|
|
33
|
+
supportedChains,
|
|
34
|
+
dstChainOptions,
|
|
35
|
+
bridgeTokensDictionary,
|
|
36
|
+
} = useSwapContext();
|
|
37
|
+
|
|
38
|
+
const type = activePicker ?? "source";
|
|
39
|
+
// In bridge mode the destination token is derived from the source token,
|
|
40
|
+
// so the destination side picks a chain instead of a token.
|
|
41
|
+
const bridgeDestination = bridgeUI && type === "destination";
|
|
42
|
+
const chain = type === "source" ? srcChain : dstChain;
|
|
43
|
+
const token = type === "source" ? srcToken : dstToken;
|
|
44
|
+
const tokens =
|
|
45
|
+
type === "source" ? srcChainTokensOptions : dstChainTokensOptions;
|
|
46
|
+
const otherTokens =
|
|
47
|
+
type === "source" ? srcChainOtherTokensOptions : dstChainOtherTokensOptions;
|
|
48
|
+
|
|
49
|
+
const close = () => setActivePicker(null);
|
|
50
|
+
|
|
51
|
+
const onTokenSelect = (token: TokenOption) => {
|
|
52
|
+
const newChain = supportedChains.find(
|
|
53
|
+
(chain) => chain.chainId === token.chainId,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (type === "source") {
|
|
57
|
+
setSrcToken(token);
|
|
58
|
+
setSrcChain(newChain);
|
|
59
|
+
// The new source token cannot reach the current destination —
|
|
60
|
+
// reset the destination so the user picks a reachable one.
|
|
61
|
+
if (
|
|
62
|
+
dstChain &&
|
|
63
|
+
(!isChainConnectionSupported(newChain, dstChain.chainId, bridgeUI) ||
|
|
64
|
+
(bridgeUI &&
|
|
65
|
+
!isBridgeTokenConnectionSupported({
|
|
66
|
+
token,
|
|
67
|
+
tokenChain: newChain,
|
|
68
|
+
dstChainId: dstChain.chainId,
|
|
69
|
+
bridgeTokensDictionary,
|
|
70
|
+
})))
|
|
71
|
+
) {
|
|
72
|
+
setDstChain(undefined);
|
|
73
|
+
setDstToken(undefined);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
setDstToken(token);
|
|
77
|
+
setDstChain(newChain);
|
|
78
|
+
// The current source cannot reach the new destination chain —
|
|
79
|
+
// reset the source so the user picks a reachable one.
|
|
80
|
+
if (
|
|
81
|
+
srcChain &&
|
|
82
|
+
newChain &&
|
|
83
|
+
!isChainConnectionSupported(srcChain, newChain.chainId, bridgeUI)
|
|
84
|
+
) {
|
|
85
|
+
setSrcChain(undefined);
|
|
86
|
+
setSrcToken(undefined);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
close();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onDstChainSelect = (newChain: Chain & { disabled?: boolean }) => {
|
|
93
|
+
// The current source cannot reach the new destination chain —
|
|
94
|
+
// reset the source so the user picks a reachable one.
|
|
95
|
+
if (newChain.disabled || newChain.chainId === srcChain?.chainId) {
|
|
96
|
+
setSrcChain(undefined);
|
|
97
|
+
setSrcToken(undefined);
|
|
98
|
+
}
|
|
99
|
+
setDstChain(newChain);
|
|
100
|
+
// The destination token gets derived from the source token and the
|
|
101
|
+
// new chain by the SwapProvider validation effect.
|
|
102
|
+
setDstToken(undefined);
|
|
103
|
+
close();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!activePicker) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
className="fixed top-0 left-0 right-0 bottom-0 z-10 flex flex-col justify-end bg-[rgba(2,2,2,0.8)] sm:absolute sm:z-[1] sm:justify-start sm:bg-transparent"
|
|
113
|
+
onMouseDown={(e) => {
|
|
114
|
+
if (e.target === e.currentTarget) {
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
e.nativeEvent.stopImmediatePropagation();
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
close();
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<div className="flex flex-col w-full h-[85vh] max-h-full overflow-hidden p-4 pt-2.5 rounded-t-[24px] border-t border-solid border-t_text_primary border-opacity-10 bg-gradient-to-r from-t_bg_secondary/50 to-t_bg_secondary/50 bg-t_bg_primary sm:h-full sm:p-0 sm:rounded-none sm:border-0 sm:bg-none sm:bg-transparent">
|
|
123
|
+
<div className="sm:hidden mx-auto mb-4 h-1 w-8 rounded-full bg-t_bg_tertiary bg-opacity-10 shrink-0" />
|
|
124
|
+
{bridgeDestination ? (
|
|
125
|
+
<ChainPicker
|
|
126
|
+
chains={dstChainOptions}
|
|
127
|
+
selectedChainId={dstChain?.chainId}
|
|
128
|
+
sameChainId={srcChain?.chainId}
|
|
129
|
+
onSelect={onDstChainSelect}
|
|
130
|
+
onClose={close}
|
|
131
|
+
/>
|
|
132
|
+
) : (
|
|
133
|
+
<TokenPicker
|
|
134
|
+
type={type}
|
|
135
|
+
chain={chain}
|
|
136
|
+
tokens={tokens}
|
|
137
|
+
otherTokens={otherTokens}
|
|
138
|
+
selectedTokenAddress={token?.address}
|
|
139
|
+
onSelect={onTokenSelect}
|
|
140
|
+
isOpen
|
|
141
|
+
setModalOpen={(open) => {
|
|
142
|
+
if (!open) {
|
|
143
|
+
close();
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -15,7 +15,7 @@ export const Delivery = () => {
|
|
|
15
15
|
</h2>
|
|
16
16
|
<div className="w-0.5 h-0.5 bg-t_text_primary opacity-25 shrink-0" />
|
|
17
17
|
<p className="text-sm leading-6 tracking-[0.01em] text-t_text_primary text-opacity-50 whitespace-nowrap truncate">
|
|
18
|
-
{expressDelivery ? "
|
|
18
|
+
{expressDelivery ? "Est. Time 30 Sec." : "Est. Time 30 Min."}
|
|
19
19
|
</p>
|
|
20
20
|
<Tooltip
|
|
21
21
|
text={
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
crosschainEstimationTimes,
|
|
12
12
|
} from "@src/constants";
|
|
13
13
|
import { useHistory, useSwapContext, useTxUIWrapper } from "@src/context";
|
|
14
|
-
import { ADDRESSES, XSwapRouterAbi } from "@src/contracts";
|
|
14
|
+
import { ADDRESSES, ERC20Abi, XSwapRouterAbi } from "@src/contracts";
|
|
15
15
|
import { useEvmContractApi } from "@src/hooks";
|
|
16
16
|
import useNetworks from "@src/hooks/networkManagement/useNetworks";
|
|
17
17
|
import {
|
|
18
18
|
Ecosystem,
|
|
19
|
+
TransactionRequest,
|
|
19
20
|
TxStatus,
|
|
20
21
|
Transaction as EVMTransaction,
|
|
21
22
|
} from "@src/models";
|
|
@@ -206,7 +207,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
|
|
|
206
207
|
const enqueueSwapTx = useCallback(async () => {
|
|
207
208
|
if (srcToken) {
|
|
208
209
|
if (route && route.ecosystem === Ecosystem.EVM) {
|
|
209
|
-
const enqueueSwap = () => {
|
|
210
|
+
const enqueueSwap = ({ afterApproval = false } = {}) => {
|
|
210
211
|
enqueueTransaction({
|
|
211
212
|
executeTransaction: () => {
|
|
212
213
|
if (!route.transactions.swap || !signer) {
|
|
@@ -216,6 +217,9 @@ export const TxOverview = ({ onCloseClick }: Props) => {
|
|
|
216
217
|
return evmContractApi.executeCalldata(
|
|
217
218
|
signer,
|
|
218
219
|
route.transactions.swap,
|
|
220
|
+
// right after an approval the RPC may not see the new
|
|
221
|
+
// allowance yet, so let the gas estimation retry
|
|
222
|
+
{ retryGasEstimation: afterApproval },
|
|
219
223
|
);
|
|
220
224
|
},
|
|
221
225
|
txStatus: TxStatus.SIGNING_TX,
|
|
@@ -258,7 +262,15 @@ export const TxOverview = ({ onCloseClick }: Props) => {
|
|
|
258
262
|
},
|
|
259
263
|
txStatus: TxStatus.SIGNING_APPROVAL,
|
|
260
264
|
handlers: {
|
|
261
|
-
onSuccess: () =>
|
|
265
|
+
onSuccess: async () => {
|
|
266
|
+
if (signer && route.transactions.approve) {
|
|
267
|
+
await waitForApprovalAllowance(
|
|
268
|
+
signer,
|
|
269
|
+
route.transactions.approve,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
enqueueSwap({ afterApproval: true });
|
|
273
|
+
},
|
|
262
274
|
},
|
|
263
275
|
});
|
|
264
276
|
} else {
|
|
@@ -452,7 +464,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
|
|
|
452
464
|
enqueueSwapTx();
|
|
453
465
|
}}
|
|
454
466
|
type="button"
|
|
455
|
-
className="text-
|
|
467
|
+
className="text-t_button_pr_text border-none h-14 text-sm font-bold w-full py-5 cursor-pointer bg-gradient-to-r from-t_main_accent_light to-t_main_accent_dark rounded-lg"
|
|
456
468
|
>
|
|
457
469
|
Sign and swap
|
|
458
470
|
</Button>
|
|
@@ -463,6 +475,40 @@ export const TxOverview = ({ onCloseClick }: Props) => {
|
|
|
463
475
|
);
|
|
464
476
|
};
|
|
465
477
|
|
|
478
|
+
const ALLOWANCE_POLL_INTERVAL_MS = 500;
|
|
479
|
+
const ALLOWANCE_POLL_MAX_ATTEMPTS = 10;
|
|
480
|
+
|
|
481
|
+
// After an approval is mined, RPC nodes behind a load balancer may still
|
|
482
|
+
// return the stale allowance for a while, which makes the swap's gas
|
|
483
|
+
// estimation revert. Poll until the approved amount is visible (or give up
|
|
484
|
+
// after ~5s and let the gas estimation retry handle the rest).
|
|
485
|
+
async function waitForApprovalAllowance(
|
|
486
|
+
signer: ethers.Signer,
|
|
487
|
+
approveTx: TransactionRequest,
|
|
488
|
+
) {
|
|
489
|
+
try {
|
|
490
|
+
const erc20 = new ethers.Contract(approveTx.to, ERC20Abi, signer);
|
|
491
|
+
const [spender, amount] = erc20.interface.decodeFunctionData(
|
|
492
|
+
"approve",
|
|
493
|
+
approveTx.data,
|
|
494
|
+
);
|
|
495
|
+
const owner = await signer.getAddress();
|
|
496
|
+
for (let attempt = 0; attempt < ALLOWANCE_POLL_MAX_ATTEMPTS; attempt++) {
|
|
497
|
+
const currentAllowance = await erc20.allowance(owner, spender);
|
|
498
|
+
if (currentAllowance.gte(amount)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await new Promise((resolve) =>
|
|
502
|
+
setTimeout(resolve, ALLOWANCE_POLL_INTERVAL_MS),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
// best-effort — non-standard approve calldata or a failing allowance
|
|
507
|
+
// call shouldn't block the swap
|
|
508
|
+
console.error("Failed to poll allowance after approval:", err);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
466
512
|
// Add this function to check token delegation and delegate if necessary
|
|
467
513
|
async function checkAndDelegateTokenAuthority(
|
|
468
514
|
connection: Connection,
|
|
@@ -195,7 +195,7 @@ export const FeesPanel = () => {
|
|
|
195
195
|
)}
|
|
196
196
|
{route && !isLoading && srcChain?.chainId !== dstChain?.chainId && (
|
|
197
197
|
<>
|
|
198
|
-
<
|
|
198
|
+
<span className="text-t_text_primary opacity-50">/</span>
|
|
199
199
|
<DeliveryInfo />
|
|
200
200
|
</>
|
|
201
201
|
)}
|
|
@@ -342,10 +342,10 @@ export const SwapButton = () => {
|
|
|
342
342
|
type="button"
|
|
343
343
|
disabled={button.disabled}
|
|
344
344
|
onClick={button.fn}
|
|
345
|
-
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] ${
|
|
346
346
|
button.disabled
|
|
347
347
|
? "bg-t_button_pr_off_bg text-t_button_pr_off_text"
|
|
348
|
-
: "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"
|
|
349
349
|
}`}
|
|
350
350
|
>
|
|
351
351
|
{button.text}
|
|
@@ -55,9 +55,9 @@ export const Balance = ({ type }: Props) => {
|
|
|
55
55
|
) : (
|
|
56
56
|
<p className="text-[10px] leading-4 text-right whitespace-nowrap">
|
|
57
57
|
<span className="text-t_text_primary text-opacity-50">
|
|
58
|
-
{`Balance `}
|
|
58
|
+
{`Balance. `}
|
|
59
59
|
</span>
|
|
60
|
-
<span className="text-t_text_primary text-opacity-75">
|
|
60
|
+
<span className="text-t_text_primary text-opacity-75 tracking-[0.032em]">
|
|
61
61
|
{`${balanceInfo} ${token.symbol}`}
|
|
62
62
|
</span>
|
|
63
63
|
</p>
|
|
@@ -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
|
+
};
|