@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
|
@@ -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
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CaretsUpDownIcon } from "@src/assets/icons";
|
|
2
2
|
import { useSwapContext } from "@src/context";
|
|
3
3
|
import { FC } from "react";
|
|
4
4
|
|
|
@@ -22,16 +22,14 @@ export const ReorderButton: FC = () => {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<div className="h-
|
|
25
|
+
<div className="h-0 w-full relative z-[1]">
|
|
26
26
|
<button
|
|
27
27
|
onClick={onReorderClick}
|
|
28
28
|
aria-label="Switch target token with the source token"
|
|
29
|
-
className="absolute -translate-
|
|
29
|
+
className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-md bg-t_bg_secondary border border-solid border-t_text_primary border-opacity-10 hover:border-opacity-25 transition-colors"
|
|
30
30
|
>
|
|
31
|
-
<div className="
|
|
32
|
-
<
|
|
33
|
-
<ArrowsExchange />
|
|
34
|
-
</div>
|
|
31
|
+
<div className="flex fill-t_text_primary opacity-75 [&_svg]:w-2.5 [&_svg]:h-4">
|
|
32
|
+
<CaretsUpDownIcon />
|
|
35
33
|
</div>
|
|
36
34
|
</button>
|
|
37
35
|
</div>
|
|
@@ -8,35 +8,34 @@ export const Delivery = () => {
|
|
|
8
8
|
const { expressDelivery, setExpressDelivery } = useSwapContext();
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<div className="flex
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
className="cursor-help fill-t_main_accent_light w-[18px] h-[18px]"
|
|
33
|
-
/>
|
|
34
|
-
</div>
|
|
35
|
-
<div className="text-xs font-medium">
|
|
36
|
-
<span className="text-xs font-medium opacity-60">{`Max time: `}</span>
|
|
37
|
-
{expressDelivery ? "30 sec" : "30 min"}
|
|
38
|
-
</div>
|
|
11
|
+
<div className="flex gap-2 items-center justify-between p-4 rounded-xl bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/[0.01]">
|
|
12
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
13
|
+
<h2 className="text-sm leading-6 tracking-[0.01em] text-t_text_primary whitespace-nowrap">
|
|
14
|
+
Fast Delivery
|
|
15
|
+
</h2>
|
|
16
|
+
<div className="w-0.5 h-0.5 bg-t_text_primary opacity-25 shrink-0" />
|
|
17
|
+
<p className="text-sm leading-6 tracking-[0.01em] text-t_text_primary text-opacity-50 whitespace-nowrap truncate">
|
|
18
|
+
{expressDelivery ? "Est. Time 30 Sec." : "Est. Time 30 Min."}
|
|
19
|
+
</p>
|
|
20
|
+
<Tooltip
|
|
21
|
+
text={
|
|
22
|
+
"Fast Delivery is a special feature of XSwap that reduces transaction time across chains to around 30 seconds. It is currently available for swaps below a value of $1000 USD."
|
|
23
|
+
}
|
|
24
|
+
position="BOTTOM"
|
|
25
|
+
triggerRef={tooltipTriggerRef}
|
|
26
|
+
id="express-delivery-tooltip"
|
|
27
|
+
/>
|
|
28
|
+
<HelpIcon
|
|
29
|
+
ref={tooltipTriggerRef}
|
|
30
|
+
className="cursor-help fill-t_text_primary opacity-30 w-4 h-4 shrink-0"
|
|
31
|
+
/>
|
|
39
32
|
</div>
|
|
33
|
+
<ToggleButton
|
|
34
|
+
checked={expressDelivery}
|
|
35
|
+
onClick={() => {
|
|
36
|
+
setExpressDelivery((x) => !x);
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
40
39
|
</div>
|
|
41
40
|
);
|
|
42
41
|
};
|
|
@@ -9,31 +9,34 @@ export const InfiniteApproval = () => {
|
|
|
9
9
|
const { infiniteApproval, setInfiniteApproval } = useSwapContext();
|
|
10
10
|
|
|
11
11
|
return (
|
|
12
|
-
<div className="flex
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
className="cursor-help fill-t_main_accent_light w-[18px] h-[18px]"
|
|
34
|
-
/>
|
|
35
|
-
</div>
|
|
12
|
+
<div className="flex gap-2 items-center justify-between p-4 rounded-xl bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/[0.01]">
|
|
13
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
14
|
+
<h2 className="text-sm leading-6 tracking-[0.01em] text-t_text_primary whitespace-nowrap">
|
|
15
|
+
Persistent Access
|
|
16
|
+
</h2>
|
|
17
|
+
<div className="w-0.5 h-0.5 bg-t_text_primary opacity-25 shrink-0" />
|
|
18
|
+
<p className="text-sm leading-6 tracking-[0.01em] text-t_text_primary text-opacity-50 whitespace-nowrap truncate">
|
|
19
|
+
Don't Ask Every Tx.
|
|
20
|
+
</p>
|
|
21
|
+
<Tooltip
|
|
22
|
+
text={
|
|
23
|
+
"Enabling persistent access (infinite approval) allows to access your tokens without requiring repeated approvals for each transaction. This streamlines multiple transactions but may pose a security risk if the spender's address is compromised."
|
|
24
|
+
}
|
|
25
|
+
position="BOTTOM"
|
|
26
|
+
triggerRef={tooltipTriggerRef}
|
|
27
|
+
id="infinite-approval-tooltip"
|
|
28
|
+
/>
|
|
29
|
+
<HelpIcon
|
|
30
|
+
ref={tooltipTriggerRef}
|
|
31
|
+
className="cursor-help fill-t_text_primary opacity-30 w-4 h-4 shrink-0"
|
|
32
|
+
/>
|
|
36
33
|
</div>
|
|
34
|
+
<ToggleButton
|
|
35
|
+
checked={infiniteApproval}
|
|
36
|
+
onClick={() => {
|
|
37
|
+
setInfiniteApproval((x) => !x);
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
37
40
|
</div>
|
|
38
41
|
);
|
|
39
42
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { AdjustmentsIcon } from "@src/assets/icons";
|
|
1
2
|
import { SafeInput } from "@src/components";
|
|
2
3
|
import { NUMBER_INPUT_REGEX } from "@src/constants";
|
|
3
4
|
import { useSwapContext } from "@src/context";
|
|
4
5
|
import { useState } from "react";
|
|
5
6
|
|
|
7
|
+
const defaultPreset = "1.5";
|
|
8
|
+
const presets = ["1", "2", "6"];
|
|
9
|
+
|
|
6
10
|
export const Slippage = () => {
|
|
7
11
|
const { slippage, setSlippage } = useSwapContext();
|
|
8
12
|
const [customSlippage, setCustomSlippage] = useState(slippage);
|
|
@@ -22,63 +26,60 @@ export const Slippage = () => {
|
|
|
22
26
|
}
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
const defaultPreset = "1.5";
|
|
26
|
-
const presets = ["0.5", "1.5", "3.0"];
|
|
27
29
|
return (
|
|
28
|
-
<div className="flex flex-col gap-5
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
<div className="flex flex-col gap-5 p-4 rounded-xl bg-gradient-to-b from-t_bg_tertiary/[0.02] to-t_bg_tertiary/0">
|
|
31
|
+
<div className="flex items-center gap-2">
|
|
32
|
+
<h2 className="text-sm leading-5 tracking-[0.01em] text-t_text_primary">
|
|
33
|
+
Price Tolerance
|
|
34
|
+
</h2>
|
|
35
|
+
<div className="w-0.5 h-0.5 bg-t_text_primary opacity-25 shrink-0" />
|
|
36
|
+
<p className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50">
|
|
37
|
+
{`${Number(slippage).toFixed(2)}%`}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="flex items-center gap-4 w-full">
|
|
41
|
+
<div className="flex items-center justify-between flex-1 gap-3 p-3 rounded-[10px] bg-t_bg_tertiary bg-opacity-5 border border-solid border-t_text_primary border-opacity-25">
|
|
42
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
43
|
+
<div className="flex items-center justify-center w-6 h-6 rounded bg-t_bg_tertiary bg-opacity-5 shrink-0">
|
|
44
|
+
<AdjustmentsIcon className="w-3.5 h-3.5 text-t_text_primary opacity-70" />
|
|
45
|
+
</div>
|
|
46
|
+
<SafeInput
|
|
47
|
+
id="slippage-input"
|
|
48
|
+
placeholder="0.00"
|
|
49
|
+
regex={NUMBER_INPUT_REGEX}
|
|
50
|
+
value={customSlippage}
|
|
51
|
+
onChange={onInputChange}
|
|
52
|
+
className="text-sm leading-6 bg-transparent border-none px-0 w-full min-w-0 placeholder:text-t_text_primary placeholder:text-opacity-50 focus:outline-none text-t_text_primary"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<span className="text-sm leading-6 text-t_text_primary text-opacity-50 shrink-0">
|
|
56
|
+
%
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="w-0.5 h-0.5 bg-t_text_primary opacity-25 shrink-0 hidden sm:block" />
|
|
60
|
+
<div className="flex items-center gap-2">
|
|
32
61
|
{presets.map((preset) => (
|
|
33
62
|
<button
|
|
34
63
|
key={preset}
|
|
35
64
|
type="button"
|
|
36
65
|
onClick={() => onInputChange(preset)}
|
|
37
|
-
className={`
|
|
66
|
+
className={`w-14 sm:w-[72px] px-3 py-2.5 rounded-lg text-sm leading-5 tracking-[0.06em] text-center transition-colors ${
|
|
38
67
|
slippage === preset
|
|
39
|
-
? "gradient-
|
|
40
|
-
: "text-t_text_primary opacity-
|
|
68
|
+
? "bg-gradient-to-r from-t_main_accent_light/40 to-t_main_accent_dark/60 text-t_text_primary"
|
|
69
|
+
: "bg-t_bg_tertiary bg-opacity-5 text-t_text_primary text-opacity-75 hover:bg-opacity-10"
|
|
41
70
|
}`}
|
|
42
71
|
>
|
|
43
72
|
{`${preset}%`}
|
|
44
73
|
</button>
|
|
45
74
|
))}
|
|
46
75
|
</div>
|
|
47
|
-
<div
|
|
48
|
-
className={`${
|
|
49
|
-
customSlippage
|
|
50
|
-
? "gradient-slippage-container-blue p-[1px] rounded-xl"
|
|
51
|
-
: ""
|
|
52
|
-
}`}
|
|
53
|
-
>
|
|
54
|
-
<div
|
|
55
|
-
className={`bg-t_bg_primary p-1 h-full border border-solid border-t_text_primary border-opacity-10 rounded-xl`}
|
|
56
|
-
>
|
|
57
|
-
<SafeInput
|
|
58
|
-
id="slippage-input"
|
|
59
|
-
placeholder="1.5"
|
|
60
|
-
regex={NUMBER_INPUT_REGEX}
|
|
61
|
-
value={customSlippage}
|
|
62
|
-
onChange={onInputChange}
|
|
63
|
-
className={`text-sm leading-none bg-transparent px-0.5 placeholder:text-t_text_primary/20 focus:outline-none ${
|
|
64
|
-
customSlippage ? "text-t_text_primary" : "text-white"
|
|
65
|
-
} `}
|
|
66
|
-
/>{" "}
|
|
67
|
-
%
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
<div className="text-sm opacity-60">
|
|
72
|
-
<p>
|
|
73
|
-
Slippage is the price variation you are willing to accept in the event
|
|
74
|
-
that the price of the trade changes while it is processing.
|
|
75
|
-
</p>
|
|
76
|
-
<br />
|
|
77
|
-
<p>
|
|
78
|
-
If the trade fails due to too low slippage, you will receive USDC on
|
|
79
|
-
the destination chain.
|
|
80
|
-
</p>
|
|
81
76
|
</div>
|
|
77
|
+
<p className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-40">
|
|
78
|
+
<span className="font-medium">Price Tolerance</span>
|
|
79
|
+
{` defines how much a price can change before your transfer falls back
|
|
80
|
+
to USDC. If the price moves more than your tolerance allows, you'll
|
|
81
|
+
receive USDC on your destination chain.`}
|
|
82
|
+
</p>
|
|
82
83
|
</div>
|
|
83
84
|
);
|
|
84
85
|
};
|
|
@@ -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,
|
|
@@ -1,27 +1,17 @@
|
|
|
1
|
-
import { TimerIcon } from "@src/assets/icons";
|
|
2
1
|
import { useSwapContext } from "@src/context";
|
|
3
2
|
|
|
4
3
|
export const DeliveryInfo = () => {
|
|
5
4
|
const { isExpressDeliveryActive } = useSwapContext();
|
|
6
5
|
|
|
7
6
|
return (
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</div>
|
|
18
|
-
<span
|
|
19
|
-
className={`text-xs font-medium sm:text-sm ${
|
|
20
|
-
isExpressDeliveryActive ? "text-t_success_light" : "opacity-50"
|
|
21
|
-
}`}
|
|
22
|
-
>
|
|
23
|
-
{isExpressDeliveryActive ? "Fast ~ 30sec " : "Normal ~ 30min"}
|
|
24
|
-
</span>
|
|
25
|
-
</div>
|
|
7
|
+
<span
|
|
8
|
+
className={`text-sm leading-6 tracking-[0.01em] whitespace-nowrap ${
|
|
9
|
+
isExpressDeliveryActive
|
|
10
|
+
? "text-t_success_light"
|
|
11
|
+
: "text-t_text_primary opacity-50"
|
|
12
|
+
}`}
|
|
13
|
+
>
|
|
14
|
+
{isExpressDeliveryActive ? "Est. Time 30 Sec." : "Est. Time 30 Min."}
|
|
15
|
+
</span>
|
|
26
16
|
);
|
|
27
17
|
};
|
|
@@ -8,10 +8,10 @@ type Props = {
|
|
|
8
8
|
export const Fee = ({ name, value }: Props) => {
|
|
9
9
|
const { isFetchingRoute } = useSwapContext();
|
|
10
10
|
return (
|
|
11
|
-
<div className="flex gap-x-1 justify-between text-
|
|
12
|
-
<div className="opacity-
|
|
13
|
-
<div>
|
|
14
|
-
{isFetchingRoute ? <Skeleton className="w-[60px] h-
|
|
11
|
+
<div className="flex gap-x-1 justify-between text-sm leading-6 tracking-[0.01em]">
|
|
12
|
+
<div className="text-t_text_primary text-opacity-50">{name}</div>
|
|
13
|
+
<div className="text-t_text_primary text-opacity-80">
|
|
14
|
+
{isFetchingRoute ? <Skeleton className="w-[60px] h-[20px]" /> : value}
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
);
|
|
@@ -24,7 +24,7 @@ export const Fees = ({
|
|
|
24
24
|
} ${feeToken?.symbol || ""}`,
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
name: "
|
|
27
|
+
name: "Net. Fee: ",
|
|
28
28
|
value: `${
|
|
29
29
|
weiToHumanReadable({
|
|
30
30
|
amount: route?.xSwapFees.xSwapFee.nativeFee || "0",
|
|
@@ -47,9 +47,9 @@ export const Fees = ({
|
|
|
47
47
|
},
|
|
48
48
|
]
|
|
49
49
|
: []),
|
|
50
|
-
{ name: "
|
|
50
|
+
{ name: "Price Tolerance: ", value: `${slippage}%` },
|
|
51
51
|
{
|
|
52
|
-
name: "Min
|
|
52
|
+
name: "Min. Received: ",
|
|
53
53
|
value: `${
|
|
54
54
|
weiToHumanReadable({
|
|
55
55
|
amount: route?.minAmountOut || "0",
|
|
@@ -61,12 +61,8 @@ export const Fees = ({
|
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
|
-
<div className="border-t border-solid border-t_text_primary border-opacity-10 pt-3 mt-
|
|
65
|
-
<div
|
|
66
|
-
className={`flex flex-col gap-y-2 gap-x-10 sm:grid ${
|
|
67
|
-
fees.length === 4 ? "sm:grid-rows-2" : "sm:grid-rows-3"
|
|
68
|
-
} sm:grid-flow-col`}
|
|
69
|
-
>
|
|
64
|
+
<div className="border-t border-solid border-t_text_primary border-opacity-10 pt-3 mt-2 mb-1">
|
|
65
|
+
<div className="flex flex-col gap-y-2">
|
|
70
66
|
{fees.map(({ name, value }) => (
|
|
71
67
|
<Fee key={name} name={name} value={value} />
|
|
72
68
|
))}
|