@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
@@ -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 { ArrowsExchange } from "@src/assets/icons";
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-[1px] w-full">
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-y-5 left-1/2 -translate-x-5 p-1 border border-solid border-t_text_primary border-opacity-10 rounded-xl bg-t_bg_secondary"
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="relative bg-gradient-to-r from-t_main_accent_light to-t_main_accent_dark w-8 h-8 rounded-lg">
32
- <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 fill-white">
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 flex-col gap-5 pt-5 border-t border-solid border-t_text_primary border-opacity-10">
12
- <h1 className="text-sm font-medium text-left">Delivery</h1>
13
- <div className="flex gap-2 items-center justify-between flex-wrap">
14
- <div className="flex items-center">
15
- <h2 className="text-xs">Express delivery: </h2>
16
- <ToggleButton
17
- checked={expressDelivery}
18
- onClick={() => {
19
- setExpressDelivery((x) => !x);
20
- }}
21
- />
22
- <Tooltip
23
- text={
24
- "Express 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."
25
- }
26
- position="BOTTOM"
27
- triggerRef={tooltipTriggerRef}
28
- id="express-delivery-tooltip"
29
- />
30
- <HelpIcon
31
- ref={tooltipTriggerRef}
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 flex-col gap-5 pt-5 border-t border-solid border-white border-opacity-10">
13
- <h1 className="text-sm font-medium text-left">Approval</h1>
14
- <div className="flex gap-2 items-center justify-between flex-wrap">
15
- <div className="flex items-center">
16
- <h2 className="text-xs">Infinite approval: </h2>
17
- <ToggleButton
18
- checked={infiniteApproval}
19
- onClick={() => {
20
- setInfiniteApproval((x) => !x);
21
- }}
22
- />
23
- <Tooltip
24
- text={
25
- "Enabling 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."
26
- }
27
- position="BOTTOM"
28
- triggerRef={tooltipTriggerRef}
29
- id="infinite-approval-tooltip"
30
- />
31
- <HelpIcon
32
- ref={tooltipTriggerRef}
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 pt-5 border-t border-solid border-t_text_primary border-opacity-10">
29
- <h1 className="text-sm font-medium text-left">Slippage</h1>
30
- <div className="flex items-center gap-2 w-full">
31
- <div className="flex items-center bg-t_bg_tertiary bg-opacity-5 p-1 border border-solid border-t_text_primary border-opacity-10 rounded-xl gap-2">
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={`block rounded-lg text-sm bg-transparent h-7 px-1 hover:bg-gradient-to-r hover:from-t_main_accent_light hover:to-t_main_accent_dark hover:opacity-100 ${
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-slippage-container-blue text-white opacity-100"
40
- : "text-t_text_primary opacity-20"
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
  };
@@ -4,7 +4,7 @@ import { Slippage } from "./Slippage";
4
4
 
5
5
  export const SettingsView = () => {
6
6
  return (
7
- <div className="flex flex-col gap-5 px-4">
7
+ <div className="flex flex-col gap-2">
8
8
  <Delivery />
9
9
  <InfiniteApproval />
10
10
  <Slippage />
@@ -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: () => enqueueSwap(),
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-white 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"
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
- <div className="flex items-center">
9
- <div
10
- className={`w-4 h-4 mr-1 ${
11
- isExpressDeliveryActive
12
- ? "fill-t_success_light"
13
- : "fill-t_text_primary opacity-50"
14
- } `}
15
- >
16
- <TimerIcon />
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-xs font-medium">
12
- <div className="opacity-60">{name}</div>
13
- <div>
14
- {isFetchingRoute ? <Skeleton className="w-[60px] h-full" /> : value}
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: "Native Fee: ",
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: "Slippage: ", value: `${slippage}%` },
50
+ { name: "Price Tolerance: ", value: `${slippage}%` },
51
51
  {
52
- name: "Min out: ",
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-3">
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
  ))}