@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.
- package/.eslintrc.json +1 -0
- package/CHANGELOG.md +16 -0
- package/README.md +22 -4
- package/dist/index.d.mts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.global.js +94 -88
- package/dist/index.js +1378 -985
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1256 -863
- package/dist/index.mjs.map +1 -1
- package/example/index.html +12 -0
- package/example/package.json +24 -0
- package/example/pnpm-lock.yaml +1164 -0
- package/example/src/App.tsx +421 -0
- package/example/src/main.tsx +8 -0
- package/example/vite.config.ts +9 -0
- package/package.json +2 -1
- package/postcss.config.js +2 -0
- package/src/assets/icons/ChevronRightThinIcon.tsx +23 -0
- package/src/assets/icons/InfoThinIcon.tsx +33 -0
- package/src/assets/icons/RedirectThinIcon.tsx +15 -0
- package/src/assets/icons/index.ts +3 -0
- package/src/components/Swap/Header/index.tsx +3 -1
- package/src/components/Swap/HistoryView/ActivityCard/index.tsx +2 -2
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +4 -1
- package/src/components/Swap/SettingsView/Delivery/index.tsx +3 -3
- package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +3 -3
- package/src/components/Swap/SettingsView/Slippage/index.tsx +5 -3
- package/src/components/Swap/SwapView/ConfirmationView/TokenTiles/index.tsx +5 -2
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +1 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +12 -7
- package/src/components/Swap/SwapView/ConfirmationView/TxProgress/index.tsx +136 -0
- package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +108 -26
- package/src/components/Swap/SwapView/ConfirmationView/index.tsx +19 -4
- package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +12 -6
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +3 -1
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +9 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +1 -1
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +52 -42
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +1 -1
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +5 -5
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +24 -2
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +14 -3
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +14 -2
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +1 -1
- package/src/components/ToggleButton/index.tsx +5 -3
- package/src/components/TxModal/index.tsx +2 -6
- package/src/components/global.css +85 -0
- package/src/config/fonts.ts +15 -2
- package/src/context/SwapProvider.tsx +22 -7
- package/src/context/TxUIWrapper.tsx +34 -4
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCrossChainDelivery.ts +143 -0
- package/src/models/Route.ts +13 -0
- package/src/models/TransactionDelivery.ts +40 -0
- package/src/models/index.ts +1 -0
- package/src/services/api.ts +23 -0
- 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-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
106
|
+
!!note && (
|
|
37
107
|
<div className="text-sm leading-5 tracking-[0.01em] text-t_text_primary text-opacity-50">
|
|
38
|
-
{
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
11
|
-
|
|
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
|
-
...(
|
|
42
|
+
...(expressDeliveryFee.gt(0)
|
|
37
43
|
? [
|
|
38
44
|
{
|
|
39
45
|
name: "Express delivery Fee: ",
|
|
40
46
|
value: `${
|
|
41
47
|
weiToHumanReadable({
|
|
42
|
-
amount:
|
|
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: "
|
|
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
|
package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx
CHANGED
|
@@ -99,58 +99,68 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
|
|
|
99
99
|
</button>
|
|
100
100
|
|
|
101
101
|
{open && (
|
|
102
|
-
<div className="absolute right-
|
|
103
|
-
<div className="flex items-center gap-2 px-
|
|
104
|
-
<div className="text-t_text_primary opacity-60 [&_svg]:w-
|
|
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-
|
|
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: "
|
|
120
|
+
style={{ height: "214px" }}
|
|
117
121
|
data={[undefined, ...filteredChains] as (Chain | undefined)[]}
|
|
118
|
-
itemContent={(_, chain) =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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-[#
|
|
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">
|