@xswap-link/sdk 0.15.0 → 0.15.2

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 (58) hide show
  1. package/.eslintrc.json +1 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +22 -4
  4. package/dist/index.d.mts +47 -1
  5. package/dist/index.d.ts +47 -1
  6. package/dist/index.global.js +94 -88
  7. package/dist/index.js +1378 -985
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1256 -863
  10. package/dist/index.mjs.map +1 -1
  11. package/example/index.html +12 -0
  12. package/example/package.json +24 -0
  13. package/example/pnpm-lock.yaml +1164 -0
  14. package/example/src/App.tsx +421 -0
  15. package/example/src/main.tsx +8 -0
  16. package/example/vite.config.ts +9 -0
  17. package/package.json +2 -1
  18. package/postcss.config.js +2 -0
  19. package/src/assets/icons/ChevronRightThinIcon.tsx +23 -0
  20. package/src/assets/icons/InfoThinIcon.tsx +33 -0
  21. package/src/assets/icons/RedirectThinIcon.tsx +15 -0
  22. package/src/assets/icons/index.ts +3 -0
  23. package/src/components/Swap/Header/index.tsx +3 -1
  24. package/src/components/Swap/HistoryView/ActivityCard/index.tsx +2 -2
  25. package/src/components/Swap/ReorderButton/ReorderButton.tsx +4 -1
  26. package/src/components/Swap/SettingsView/Delivery/index.tsx +3 -3
  27. package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +3 -3
  28. package/src/components/Swap/SettingsView/Slippage/index.tsx +5 -3
  29. package/src/components/Swap/SwapView/ConfirmationView/TokenTiles/index.tsx +5 -2
  30. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +1 -1
  31. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +12 -7
  32. package/src/components/Swap/SwapView/ConfirmationView/TxProgress/index.tsx +136 -0
  33. package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +108 -26
  34. package/src/components/Swap/SwapView/ConfirmationView/index.tsx +19 -4
  35. package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +12 -6
  36. package/src/components/Swap/SwapView/FeesPanel/index.tsx +3 -1
  37. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +9 -0
  38. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +1 -1
  39. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +52 -42
  40. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +1 -1
  41. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +5 -5
  42. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +24 -2
  43. package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +14 -3
  44. package/src/components/Swap/SwapView/SwapPanel/index.tsx +14 -2
  45. package/src/components/Swap/SwapView/WalletPicker/index.tsx +1 -1
  46. package/src/components/ToggleButton/index.tsx +5 -3
  47. package/src/components/TxModal/index.tsx +2 -6
  48. package/src/components/global.css +85 -0
  49. package/src/config/fonts.ts +15 -2
  50. package/src/context/SwapProvider.tsx +22 -7
  51. package/src/context/TxUIWrapper.tsx +34 -4
  52. package/src/hooks/index.ts +1 -0
  53. package/src/hooks/useCrossChainDelivery.ts +143 -0
  54. package/src/models/Route.ts +13 -0
  55. package/src/models/TransactionDelivery.ts +40 -0
  56. package/src/models/index.ts +1 -0
  57. package/src/services/api.ts +23 -0
  58. package/tailwind.config.js +3 -9
@@ -57,9 +57,12 @@ export const TokenTiles = () => {
57
57
  return (
58
58
  <div className="flex flex-col gap-1.5">
59
59
  <TokenTile type="source" />
60
- {/* Zero-height anchor between the tiles, like ReorderButton in SwapView */}
60
+ {/* Zero-height anchor between the tiles, like ReorderButton in SwapView.
61
+ Opaque bg on purpose: Chromium ignores backdrop-filter nested inside
62
+ the widget's own backdrop-blur, so a translucent bg would let the
63
+ tile edges show through the marker. */}
61
64
  <div className="h-0 w-full relative z-[1]">
62
- <div className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-[24px] bg-t_bg_secondary bg-opacity-50 bg-gradient-to-b from-t_bg_tertiary/10 to-t_bg_tertiary/[0.08]">
65
+ <div className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-md backdrop-blur-[24px] bg-t_bg_secondary bg-gradient-to-b from-t_bg_tertiary/10 to-t_bg_tertiary/[0.08]">
63
66
  <ChevronDownWideThinIcon className="w-6 h-6 text-t_text_primary opacity-50" />
64
67
  </div>
65
68
  </div>
@@ -7,7 +7,7 @@ type Props = {
7
7
  export const Header = ({ onCloseClick }: Props) => {
8
8
  return (
9
9
  <div className="flex justify-between items-center gap-6">
10
- <div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
10
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em] text-t_text_primary">
11
11
  Confirmation
12
12
  </div>
13
13
  <button
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  CCIP_ROUTER_PROGRAM_ID,
3
+ EXPRESS_DELIVERY_TIME,
3
4
  FALLBACK_CROSSCHAIN_ESTIMATION_TIME,
4
5
  FEE_QUOTER_PROGRAM_ID,
5
6
  LINK_TOKEN_MINT,
@@ -69,6 +70,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
69
70
  route,
70
71
  refreshBalance,
71
72
  bridgeUI,
73
+ isExpressDeliveryActive,
72
74
  } = useSwapContext();
73
75
 
74
76
  const {
@@ -107,13 +109,15 @@ export const TxOverview = ({ onCloseClick }: Props) => {
107
109
  tokenOutAddress: dstToken?.address,
108
110
  tokenOutAmount: dstValueWei,
109
111
 
110
- estimatedDeliveryTimestamp:
111
- Number(route.xSwapFees.expressDeliveryFee || "0") > 0 // express delivery fee TODO
112
- ? (Date.now() / 1000) * 1000 + 30 * 1000
113
- : (Date.now() / 1000) * 1000 +
114
- crosschainEstimationTimes?.[srcChain?.chainId]?.[
115
- dstChain?.chainId
116
- ] || FALLBACK_CROSSCHAIN_ESTIMATION_TIME,
112
+ // Read the speed off the quote, never off the express-delivery fee: a fast
113
+ // CCTP transfer pays Circle out of the bridged USDC and charges no express
114
+ // fee, so the fee test used to promise 30 minutes for a 30-second transfer.
115
+ estimatedDeliveryTimestamp: isExpressDeliveryActive
116
+ ? Date.now() + EXPRESS_DELIVERY_TIME
117
+ : Date.now() +
118
+ (crosschainEstimationTimes?.[srcChain?.chainId]?.[
119
+ dstChain?.chainId
120
+ ] || FALLBACK_CROSSCHAIN_ESTIMATION_TIME),
117
121
  status: "IN_PROGRESS",
118
122
  };
119
123
  if (srcChain.chainId === dstChain.chainId) {
@@ -199,6 +203,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
199
203
  dstValueWei,
200
204
  supportedChains,
201
205
  route,
206
+ isExpressDeliveryActive,
202
207
  ],
203
208
  );
204
209
 
@@ -0,0 +1,136 @@
1
+ import {
2
+ CheckCircleThinIcon,
3
+ ProgressRingThinIcon,
4
+ RedirectThinIcon,
5
+ } from "@src/assets/icons";
6
+ import {
7
+ crosschainEstimationTimes,
8
+ FALLBACK_CROSSCHAIN_ESTIMATION_TIME,
9
+ } from "@src/constants";
10
+ import { useSwapContext, useTxUIWrapper } from "@src/context";
11
+ import { CrossChainDelivery } from "@src/hooks";
12
+ import { useEffect, useMemo, useState } from "react";
13
+ import { TokenTiles } from "../TokenTiles";
14
+
15
+ type Props = {
16
+ onCloseClick: () => void;
17
+ delivery: CrossChainDelivery;
18
+ };
19
+
20
+ /** A fast transfer that has not landed within this much is not "about to" —
21
+ * say so, rather than leave the user watching a spinner that promised seconds. */
22
+ const FAST_DELIVERY_GRACE_MS = 2 * 60 * 1000;
23
+
24
+ const formatElapsed = (ms: number) => {
25
+ const totalSeconds = Math.floor(ms / 1000);
26
+ const minutes = Math.floor(totalSeconds / 60);
27
+ const seconds = totalSeconds % 60;
28
+ return `${minutes}:${seconds.toString().padStart(2, "0")}`;
29
+ };
30
+
31
+ /**
32
+ * The source tx is mined and the funds are in flight.
33
+ *
34
+ * This is the state a fixed "delivery in ~30 min" note used to paper over: most
35
+ * transfers land in well under a minute, and the only way to know is to ask the API
36
+ * (which also claims a CCTP transfer while it answers — see `useCrossChainDelivery`).
37
+ * So the swap is not reported as finished here; it waits for the destination.
38
+ */
39
+ export const TxProgress = ({ onCloseClick, delivery }: Props) => {
40
+ const { srcChain, dstChain, isExpressDeliveryActive } = useSwapContext();
41
+ const { txExplorerUrl } = useTxUIWrapper();
42
+
43
+ const [now, setNow] = useState(() => Date.now());
44
+ useEffect(() => {
45
+ const ticker = setInterval(() => setNow(Date.now()), 1000);
46
+ return () => clearInterval(ticker);
47
+ }, []);
48
+
49
+ const elapsedMs = delivery.startedAt
50
+ ? Math.max(0, now - delivery.startedAt)
51
+ : 0;
52
+
53
+ // The bridge's own answer once it has one (a fast burn can be attested at the
54
+ // standard tier), the quote's until then.
55
+ const isExpress =
56
+ delivery.response?.expressDelivery ?? isExpressDeliveryActive;
57
+
58
+ const estimatedMs = useMemo(
59
+ () =>
60
+ (srcChain &&
61
+ dstChain &&
62
+ crosschainEstimationTimes?.[srcChain.chainId]?.[dstChain.chainId]) ||
63
+ FALLBACK_CROSSCHAIN_ESTIMATION_TIME,
64
+ [srcChain, dstChain],
65
+ );
66
+
67
+ const isSlow = elapsedMs > (isExpress ? FAST_DELIVERY_GRACE_MS : estimatedMs);
68
+
69
+ const note = isSlow
70
+ ? "This is taking longer than usual. Nothing is lost — the transfer is still on its way."
71
+ : isExpress
72
+ ? "Fast delivery is on: funds usually arrive within a minute."
73
+ : `Funds can take up to ${Math.round(
74
+ estimatedMs / 60000,
75
+ )} minutes to arrive on ${dstChain?.displayName || "the destination chain"}.`;
76
+
77
+ return (
78
+ <div className="flex flex-col gap-8 text-t_text_primary">
79
+ <div className="flex flex-col gap-2">
80
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em]">
81
+ Delivering Funds
82
+ </div>
83
+ <div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50">
84
+ {note}
85
+ </div>
86
+ <div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-25">
87
+ You can close this window — the transfer completes on its own.
88
+ </div>
89
+ </div>
90
+ <TokenTiles />
91
+ <div className="flex flex-col rounded-xl bg-t_bg_tertiary bg-opacity-[0.02]">
92
+ <div className="flex items-center justify-between gap-4 px-4 sm:px-6 py-4 rounded-lg">
93
+ <div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50">
94
+ {`Swap confirmed on ${srcChain?.displayName || "the source chain"}`}
95
+ </div>
96
+ <CheckCircleThinIcon className="w-6 h-6 shrink-0 text-[#6AE89B]" />
97
+ </div>
98
+ <div className="flex items-center justify-between gap-4 px-4 sm:px-6 py-4 rounded-lg bg-gradient-to-r from-transparent via-t_bg_tertiary/5 to-transparent">
99
+ <div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-75">
100
+ {`Waiting for funds on ${
101
+ dstChain?.displayName || "the destination chain"
102
+ }`}
103
+ </div>
104
+ <div className="flex items-center gap-3 shrink-0">
105
+ <span className="text-sm leading-5 tracking-[0.01em] tabular-nums text-t_text_primary text-opacity-50">
106
+ {formatElapsed(elapsedMs)}
107
+ </span>
108
+ <ProgressRingThinIcon className="w-6 h-6 shrink-0 animate-spin-slow text-t_text_primary" />
109
+ </div>
110
+ </div>
111
+ </div>
112
+ <div className="h-px w-full gradient-sections-separator-container" />
113
+ <button
114
+ type="button"
115
+ onClick={onCloseClick}
116
+ className="w-full px-6 py-3 rounded-lg border-none cursor-pointer bg-t_bg_tertiary bg-opacity-5 text-sm font-medium leading-6 tracking-[0.01em] text-t_text_primary text-opacity-80 hover:bg-opacity-10 transition-colors"
117
+ >
118
+ Close
119
+ </button>
120
+ <div className="w-full flex gap-3 items-center">
121
+ <a
122
+ href={txExplorerUrl}
123
+ target="_blank"
124
+ rel="noreferrer"
125
+ className="flex items-center gap-1.5 text-sm text-t_text_primary text-opacity-25 hover:text-opacity-50 transition-colors"
126
+ >
127
+ View On Explorer{" "}
128
+ <div className="w-6 h-6 bg-t_bg_tertiary bg-opacity-5 rounded-[4px] flex items-center justify-center">
129
+ <RedirectThinIcon className="w-3 h-3" />
130
+ </div>
131
+ </a>
132
+ <div className="flex-1 h-px bg-t_text_primary bg-opacity-10" />
133
+ </div>
134
+ </div>
135
+ );
136
+ };
@@ -1,28 +1,98 @@
1
1
  import {
2
2
  CheckCircleThinIcon,
3
3
  ErrorCircleThinIcon,
4
- LinkExternalThinIcon,
4
+ RedirectThinIcon,
5
5
  } from "@src/assets/icons";
6
6
  import { useSwapContext, useTxUIWrapper } from "@src/context";
7
+ import { CrossChainDelivery } from "@src/hooks";
8
+ import { Ecosystem } from "@src/models";
9
+ import { useMemo } from "react";
7
10
  import { TokenTiles } from "../TokenTiles";
8
11
 
9
12
  type Props = {
10
13
  onCloseClick: () => void;
14
+ /**
15
+ * How the transfer actually settled. Absent for the flows whose delivery is not
16
+ * tracked — a single-chain swap, a non-EVM source, or a standalone transaction run
17
+ * through this modal — where the source receipt is the whole story.
18
+ */
19
+ delivery?: CrossChainDelivery;
11
20
  };
12
21
 
13
- export const TxResult = ({ onCloseClick }: Props) => {
22
+ export const TxResult = ({ onCloseClick, delivery }: Props) => {
14
23
  const { txError, txExplorerUrl, txMsg } = useTxUIWrapper();
15
- const { isExpressDeliveryActive, srcChain, dstChain } = useSwapContext();
24
+ const { isExpressDeliveryActive, srcChain, dstChain, supportedChains } =
25
+ useSwapContext();
26
+
27
+ const isCrossChain =
28
+ !!srcChain && !!dstChain && srcChain.chainId !== dstChain.chainId;
29
+ const failed = !!txError || !!delivery?.isFailed;
30
+ // Delivered, but as the bridged token: the destination swap never ran, and it
31
+ // never will — so this is an outcome to report, not a step still pending.
32
+ const partiallyFailed = !!delivery?.isPartiallyFailed;
33
+
34
+ const targetTx = delivery?.response?.targetTx;
35
+ const targetExplorerUrl = useMemo(() => {
36
+ if (!targetTx) {
37
+ return undefined;
38
+ }
39
+ const explorer = supportedChains.find(
40
+ (chain) =>
41
+ chain.chainId === targetTx.chainId && chain.ecosystem === Ecosystem.EVM,
42
+ )?.transactionExplorer;
43
+ return explorer ? `${explorer}/${targetTx.hash}` : undefined;
44
+ }, [supportedChains, targetTx]);
45
+
46
+ const note = useMemo(() => {
47
+ if (delivery?.isFailed) {
48
+ return `The transaction failed on ${
49
+ srcChain?.displayName || "the source chain"
50
+ }.`;
51
+ }
52
+ if (partiallyFailed) {
53
+ return `The swap on ${
54
+ dstChain?.displayName || "the destination chain"
55
+ } could not be completed, so the bridged tokens were sent to your wallet instead.`;
56
+ }
57
+ if (delivery?.isDelivered) {
58
+ return `Funds delivered on ${
59
+ dstChain?.displayName || "the destination chain"
60
+ }.`;
61
+ }
62
+ // Nothing was tracked (non-EVM source), so the estimate is all there is. Read
63
+ // the speed off the quote, never off the express-delivery fee — a fast CCTP
64
+ // transfer pays Circle out of the bridged USDC and charges no express fee.
65
+ if (isCrossChain) {
66
+ return `Approximated time of delivery: ${
67
+ isExpressDeliveryActive ? "30 sec" : "30 min"
68
+ }`;
69
+ }
70
+ return undefined;
71
+ }, [
72
+ delivery?.isDelivered,
73
+ delivery?.isFailed,
74
+ partiallyFailed,
75
+ isCrossChain,
76
+ isExpressDeliveryActive,
77
+ srcChain?.displayName,
78
+ dstChain?.displayName,
79
+ ]);
16
80
 
17
81
  return (
18
82
  <div className="flex flex-col gap-8 text-t_text_primary">
19
83
  <div className="flex flex-col gap-2">
20
84
  <div className="flex items-center gap-3">
21
- <div className="text-2xl leading-8 tracking-[0.01em]">
22
- {txError ? "Transaction Error" : "Transaction Success"}
85
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em]">
86
+ {failed
87
+ ? "Transaction Error"
88
+ : partiallyFailed
89
+ ? "Partially Completed"
90
+ : "Transaction Success"}
23
91
  </div>
24
- {txError ? (
92
+ {failed ? (
25
93
  <ErrorCircleThinIcon className="w-6 h-6 shrink-0 text-[#F44336]" />
94
+ ) : partiallyFailed ? (
95
+ <ErrorCircleThinIcon className="w-6 h-6 shrink-0 text-t_warning_light" />
26
96
  ) : (
27
97
  <CheckCircleThinIcon className="w-6 h-6 shrink-0 text-[#6AE89B]" />
28
98
  )}
@@ -33,31 +103,14 @@ export const TxResult = ({ onCloseClick }: Props) => {
33
103
  </div>
34
104
  ) : (
35
105
  !txMsg &&
36
- srcChain !== dstChain && (
106
+ !!note && (
37
107
  <div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50">
38
- {`Approximated time of delivery: ${
39
- isExpressDeliveryActive ? "30 sec" : "30 min"
40
- }`}
108
+ {note}
41
109
  </div>
42
110
  )
43
111
  )}
44
112
  </div>
45
- {!txMsg && (
46
- <div className="flex flex-col gap-4">
47
- <TokenTiles />
48
- {!txError && (
49
- <a
50
- href={txExplorerUrl}
51
- target="_blank"
52
- rel="noreferrer"
53
- className="flex items-center gap-2 self-start text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50 hover:text-opacity-80 transition-colors"
54
- >
55
- View details on Explorer
56
- <LinkExternalThinIcon className="w-3 h-3" />
57
- </a>
58
- )}
59
- </div>
60
- )}
113
+ {!txMsg && <TokenTiles />}
61
114
  <div className="h-px w-full gradient-sections-separator-container" />
62
115
  <button
63
116
  type="button"
@@ -66,6 +119,35 @@ export const TxResult = ({ onCloseClick }: Props) => {
66
119
  >
67
120
  Continue
68
121
  </button>
122
+ {!txMsg && !txError && (
123
+ <div className="w-full flex gap-3 items-center">
124
+ <a
125
+ href={txExplorerUrl}
126
+ target="_blank"
127
+ rel="noreferrer"
128
+ className="flex items-center gap-1.5 text-sm text-t_text_primary text-opacity-25 hover:text-opacity-50 transition-colors"
129
+ >
130
+ View On Explorer{" "}
131
+ <div className="w-6 h-6 bg-t_bg_tertiary bg-opacity-5 rounded-[4px] flex items-center justify-center">
132
+ <RedirectThinIcon className="w-3 h-3" />
133
+ </div>
134
+ </a>
135
+ {targetExplorerUrl && (
136
+ <a
137
+ href={targetExplorerUrl}
138
+ target="_blank"
139
+ rel="noreferrer"
140
+ className="flex items-center gap-1.5 text-sm text-t_text_primary text-opacity-25 hover:text-opacity-50 transition-colors"
141
+ >
142
+ View Delivery{" "}
143
+ <div className="w-6 h-6 bg-t_bg_tertiary bg-opacity-5 rounded-[4px] flex items-center justify-center">
144
+ <RedirectThinIcon className="w-3 h-3" />
145
+ </div>
146
+ </a>
147
+ )}
148
+ <div className="flex-1 h-px bg-t_text_primary bg-opacity-10" />
149
+ </div>
150
+ )}
69
151
  </div>
70
152
  );
71
153
  };
@@ -1,17 +1,32 @@
1
1
  import { useTxUIWrapper } from "@src/context";
2
+ import { CrossChainDelivery } from "@src/hooks";
2
3
  import { TxStatus } from "@src/models";
3
4
  import { TxOverview } from "./TxOverview";
5
+ import { TxProgress } from "./TxProgress";
4
6
  import { TxResult } from "./TxResult";
5
7
 
6
8
  type Props = {
7
9
  onCloseClick: () => void;
10
+ /** Delivery tracking for the transfer in the modal, owned by {@link TxUIWrapper}
11
+ * so the modal's own styling can follow it too. Idle unless one is in flight. */
12
+ delivery: CrossChainDelivery;
8
13
  };
9
- export const ConfirmationView = ({ onCloseClick }: Props) => {
14
+ export const ConfirmationView = ({ onCloseClick, delivery }: Props) => {
10
15
  const { txStatus, txError } = useTxUIWrapper();
11
16
 
12
- return txStatus === TxStatus.COMPLETED || txError ? (
13
- <TxResult onCloseClick={onCloseClick} />
17
+ if (txError) {
18
+ return <TxResult onCloseClick={onCloseClick} delivery={delivery} />;
19
+ }
20
+
21
+ if (txStatus !== TxStatus.COMPLETED) {
22
+ return <TxOverview onCloseClick={onCloseClick} />;
23
+ }
24
+
25
+ // The source tx is mined, which for a cross-chain swap is the halfway point, not
26
+ // the end: hold the success view until the funds actually land on the destination.
27
+ return delivery.isPolling ? (
28
+ <TxProgress onCloseClick={onCloseClick} delivery={delivery} />
14
29
  ) : (
15
- <TxOverview onCloseClick={onCloseClick} />
30
+ <TxResult onCloseClick={onCloseClick} delivery={delivery} />
16
31
  );
17
32
  };
@@ -1,5 +1,5 @@
1
1
  import { useSwapContext } from "@src/context";
2
- import { weiToHumanReadable } from "@src/utils";
2
+ import { safeBigNumberFrom, weiToHumanReadable } from "@src/utils";
3
3
  import { Fee } from "../Fee";
4
4
 
5
5
  export const Fees = ({
@@ -7,8 +7,14 @@ export const Fees = ({
7
7
  }: {
8
8
  solanaBridgeFee: string | null;
9
9
  }) => {
10
- const { route, slippage, feeToken, dstToken, expressDelivery } =
11
- useSwapContext();
10
+ const { route, slippage, feeToken, dstToken } = useSwapContext();
11
+
12
+ // Only routes that buy their speed with an express-delivery fee have a fee to show.
13
+ // A fast CCTP transfer has none — Circle's fast-transfer fee is taken out of the
14
+ // bridged USDC — so the row would read "0" on the very routes that are fastest.
15
+ const expressDeliveryFee = safeBigNumberFrom(
16
+ route?.xSwapFees.expressDeliveryFee || "0",
17
+ );
12
18
 
13
19
  const fees = [
14
20
  {
@@ -33,13 +39,13 @@ export const Fees = ({
33
39
  }) || 0
34
40
  } ${feeToken?.symbol || ""}`,
35
41
  },
36
- ...(expressDelivery
42
+ ...(expressDeliveryFee.gt(0)
37
43
  ? [
38
44
  {
39
45
  name: "Express delivery Fee: ",
40
46
  value: `${
41
47
  weiToHumanReadable({
42
- amount: route?.xSwapFees.expressDeliveryFee || "0",
48
+ amount: expressDeliveryFee.toString(),
43
49
  decimals: feeToken?.decimals || 18,
44
50
  precisionFractionalPlaces: 5,
45
51
  }) || 0
@@ -47,7 +53,7 @@ export const Fees = ({
47
53
  },
48
54
  ]
49
55
  : []),
50
- { name: "Price Tolerance: ", value: `${slippage}%` },
56
+ { name: "Slippage: ", value: `${slippage}%` },
51
57
  {
52
58
  name: "Min. Received: ",
53
59
  value: `${
@@ -200,8 +200,10 @@ export const FeesPanel = () => {
200
200
  </>
201
201
  )}
202
202
  </div>
203
+ {/* The 24px glyph has 8px of built-in whitespace on each side; pull the
204
+ right side back so the chevron sits flush with the panel padding. */}
203
205
  <div
204
- className={`flex items-center justify-center w-6 h-6 text-t_text_primary opacity-50 transition-transform ${
206
+ className={`flex items-center justify-center w-6 h-6 -mr-2 text-t_text_primary opacity-50 transition-transform ${
205
207
  expanded ? "rotate-180" : ""
206
208
  }`}
207
209
  >
@@ -22,6 +22,7 @@ export const AmountPanel = ({ type }: Props) => {
22
22
  setSrcValue,
23
23
  isFetchingRoute,
24
24
  bridgeUI,
25
+ isAsyncDataLoaded,
25
26
  } = useSwapContext();
26
27
 
27
28
  const onMaxClick = () => {
@@ -58,6 +59,14 @@ export const AmountPanel = ({ type }: Props) => {
58
59
  dstChain,
59
60
  ]);
60
61
 
62
+ if (!isAsyncDataLoaded) {
63
+ return (
64
+ <div className="flex items-center flex-1 min-w-0">
65
+ <Skeleton className="w-[100px] h-8" />
66
+ </div>
67
+ );
68
+ }
69
+
61
70
  return (
62
71
  <div className="flex items-center gap-3 flex-1 min-w-0 overflow-hidden text-left">
63
72
  {type === "source" ? (
@@ -56,7 +56,7 @@ export const ChainPicker = ({
56
56
  return (
57
57
  <>
58
58
  <div className="flex justify-between items-center mb-6">
59
- <div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
59
+ <div className="text-2xl font-medium leading-8 tracking-[0.01em] text-t_text_primary">
60
60
  Select Network
61
61
  </div>
62
62
  <button
@@ -99,58 +99,68 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
99
99
  </button>
100
100
 
101
101
  {open && (
102
- <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">
103
- <div className="flex items-center gap-2 px-4 py-3 border-b border-solid border-t_text_primary border-opacity-10">
104
- <div className="text-t_text_primary opacity-60 [&_svg]:w-3.5 [&_svg]:h-3.5 flex items-center">
102
+ <div className="absolute -right-2 top-[calc(100%+8px)] z-20 flex flex-col w-[296px] py-5 px-2.5 rounded-xl network-dropdown-surface border border-solid border-t_text_primary border-opacity-5 overflow-hidden">
103
+ <div className="flex items-center gap-2 px-2.5 pb-4">
104
+ <div className="text-t_text_primary opacity-60 [&_svg]:w-4 [&_svg]:h-4 flex items-center">
105
105
  <SearchThinIcon />
106
106
  </div>
107
107
  <input
108
108
  value={searchValue}
109
109
  onChange={(e) => setSearchValue(e.target.value)}
110
110
  placeholder={`Browse ${chains.length} Chains...`}
111
- 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
+ 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"
112
112
  />
113
113
  </div>
114
- <div className="h-[200px]">
114
+ <div className="h-px w-full bg-t_bg_tertiary bg-opacity-5 mb-2" />
115
+ {/* The design's list mask is a hard cut 6px into the sixth row —
116
+ that sliver is the hint that the list scrolls. Rows are 40px
117
+ pills around 24px of content, so the cut lands at 214px. */}
118
+ <div className="h-[214px]">
115
119
  <Virtuoso
116
- style={{ height: "200px" }}
120
+ style={{ height: "214px" }}
117
121
  data={[undefined, ...filteredChains] as (Chain | undefined)[]}
118
- itemContent={(_, chain) => (
119
- <button
120
- type="button"
121
- onClick={() => {
122
- onSelect(chain);
123
- setOpen(false);
124
- }}
125
- 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 ${
126
- (chain?.chainId || "") === (selectedChain?.chainId || "")
127
- ? "bg-t_bg_tertiary bg-opacity-10"
128
- : ""
129
- }`}
130
- >
131
- {chain ? (
132
- <img
133
- src={chain.image}
134
- alt={chain.displayName}
135
- className="w-5 h-5 rounded-full"
136
- />
137
- ) : (
138
- <div className="grid grid-cols-2 gap-0.5 shrink-0">
139
- {clusterChains.map((clusterChain) => (
140
- <img
141
- key={clusterChain.chainId}
142
- src={clusterChain.image}
143
- alt={clusterChain.displayName}
144
- className="w-[11px] h-[11px] rounded-full"
145
- />
146
- ))}
147
- </div>
148
- )}
149
- <p className="text-sm leading-6 text-t_text_primary truncate">
150
- {chain ? chain.displayName : "All Networks"}
151
- </p>
152
- </button>
153
- )}
122
+ itemContent={(_, chain) => {
123
+ const isSelected =
124
+ (chain?.chainId || "") === (selectedChain?.chainId || "");
125
+ return (
126
+ <button
127
+ type="button"
128
+ onClick={() => {
129
+ onSelect(chain);
130
+ setOpen(false);
131
+ }}
132
+ className={`network-dropdown-row flex items-center gap-2.5 w-full h-10 px-2.5 rounded-lg text-left ${
133
+ isSelected ? "network-dropdown-row-active" : ""
134
+ }`}
135
+ >
136
+ {chain ? (
137
+ <img
138
+ src={chain.image}
139
+ alt={chain.displayName}
140
+ className="w-6 h-6 rounded-full"
141
+ />
142
+ ) : (
143
+ <div className="grid grid-cols-2 gap-0.5 shrink-0">
144
+ {clusterChains.map((clusterChain) => (
145
+ <img
146
+ key={clusterChain.chainId}
147
+ src={clusterChain.image}
148
+ alt={clusterChain.displayName}
149
+ className="w-[11px] h-[11px] rounded-full"
150
+ />
151
+ ))}
152
+ </div>
153
+ )}
154
+ <p
155
+ className={`text-sm leading-[18px] tracking-[0.01em] text-t_text_primary truncate ${
156
+ isSelected ? "" : "text-opacity-80"
157
+ }`}
158
+ >
159
+ {chain ? chain.displayName : "All Networks"}
160
+ </p>
161
+ </button>
162
+ );
163
+ }}
154
164
  />
155
165
  </div>
156
166
  </div>
@@ -139,7 +139,7 @@ export const TokenItem = ({
139
139
  )
140
140
  )}
141
141
  {resetWarning ? (
142
- <div className="flex items-center justify-end gap-1.5 text-[#D77F02]">
142
+ <div className="flex items-center justify-end gap-1.5 text-[#F46F28]">
143
143
  <WarningIcon className="w-3.5 h-3.5 shrink-0" />
144
144
  <span className="text-xs leading-[18px] tracking-[0.01em] whitespace-nowrap">
145
145
  <span className="sm:hidden">