@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
|
@@ -480,7 +480,7 @@ export const TokenPicker = ({
|
|
|
480
480
|
return showImportWarning ? (
|
|
481
481
|
<>
|
|
482
482
|
<div className="flex justify-between items-center mb-6">
|
|
483
|
-
<div className="text-2xl leading-8 tracking-[0.01em]">
|
|
483
|
+
<div className="text-2xl font-medium leading-8 tracking-[0.01em]">
|
|
484
484
|
Import token
|
|
485
485
|
</div>
|
|
486
486
|
<button
|
|
@@ -570,7 +570,7 @@ export const TokenPicker = ({
|
|
|
570
570
|
) : (
|
|
571
571
|
<>
|
|
572
572
|
<div className="flex justify-between items-center mb-6">
|
|
573
|
-
<div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
|
|
573
|
+
<div className="text-2xl font-medium leading-8 tracking-[0.01em] text-t_text_primary">
|
|
574
574
|
Select Token
|
|
575
575
|
</div>
|
|
576
576
|
<button
|
|
@@ -626,7 +626,7 @@ export const TokenPicker = ({
|
|
|
626
626
|
<div className="text-sm font-medium text-ellipsis">
|
|
627
627
|
{customTokenData.name}
|
|
628
628
|
</div>
|
|
629
|
-
<div className="text-xs text-t_text_primary !text-opacity-50
|
|
629
|
+
<div className="text-xs text-t_text_primary !text-opacity-50">
|
|
630
630
|
{importChain?.displayName}
|
|
631
631
|
</div>
|
|
632
632
|
</div>
|
|
@@ -648,7 +648,7 @@ export const TokenPicker = ({
|
|
|
648
648
|
<div className="flex flex-col h-full min-h-0">
|
|
649
649
|
{/* Pinned label of the group at the top of the list; sits
|
|
650
650
|
above the scroller, so rows clip on its bottom edge. */}
|
|
651
|
-
<p className="pb-3 text-[10px]
|
|
651
|
+
<p className="pb-3 text-[10px] leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left shrink-0">
|
|
652
652
|
{pinnedGroupLabel}
|
|
653
653
|
</p>
|
|
654
654
|
<div className="relative flex flex-col flex-1 min-h-0">
|
|
@@ -663,7 +663,7 @@ export const TokenPicker = ({
|
|
|
663
663
|
}
|
|
664
664
|
if (row.kind === "label") {
|
|
665
665
|
return (
|
|
666
|
-
<p className="pt-2 pb-3 text-[10px]
|
|
666
|
+
<p className="pt-2 pb-3 text-[10px] leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
|
|
667
667
|
{row.label}
|
|
668
668
|
</p>
|
|
669
669
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChevronDownThinIcon } from "@src/assets/icons";
|
|
2
|
-
import { TokenLogoWithChain } from "@src/components";
|
|
2
|
+
import { Skeleton, TokenLogoWithChain } from "@src/components";
|
|
3
3
|
import { useSwapContext } from "@src/context";
|
|
4
4
|
import { Chain, Token } from "@src/models";
|
|
5
5
|
import { useMemo } from "react";
|
|
@@ -19,6 +19,7 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
|
|
|
19
19
|
srcTokenLocked,
|
|
20
20
|
dstChainLocked,
|
|
21
21
|
setActivePicker,
|
|
22
|
+
isAsyncDataLoaded,
|
|
22
23
|
} = useSwapContext();
|
|
23
24
|
|
|
24
25
|
const integratorLockedDestinationToken = useMemo(
|
|
@@ -43,6 +44,25 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
|
|
|
43
44
|
integratorLockedDestinationToken ||
|
|
44
45
|
integratorLockedSourceToken;
|
|
45
46
|
|
|
47
|
+
if (!isAsyncDataLoaded) {
|
|
48
|
+
// Mirror the loaded layout: symbol bar + token circle with the small
|
|
49
|
+
// chain badge, so nothing shifts when the real content arrives.
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`flex items-center shrink-0 min-h-11 ${className || ""}`}
|
|
53
|
+
aria-hidden
|
|
54
|
+
>
|
|
55
|
+
<div className="flex items-center gap-3">
|
|
56
|
+
<Skeleton className="w-12 h-4" />
|
|
57
|
+
<div className="relative flex items-end pr-1 pb-1">
|
|
58
|
+
<Skeleton className="w-10 h-10 rounded-full" />
|
|
59
|
+
<Skeleton className="absolute bottom-0 right-0 w-[18px] h-[18px] rounded-full border border-solid border-t_bg_secondary" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
return (
|
|
47
67
|
<button
|
|
48
68
|
type="button"
|
|
@@ -86,7 +106,9 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
|
|
|
86
106
|
)}
|
|
87
107
|
</div>
|
|
88
108
|
{!locked && (
|
|
89
|
-
|
|
109
|
+
// The 24px glyph has 8px of built-in whitespace on each side; pull the
|
|
110
|
+
// right side back so the chevron sits flush with the panel edge.
|
|
111
|
+
<div className="flex items-center justify-center w-6 h-6 ml-1 -mr-2 text-t_text_primary opacity-50">
|
|
90
112
|
<ChevronDownThinIcon />
|
|
91
113
|
</div>
|
|
92
114
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChevronDownThinSmallIcon } from "@src/assets/icons";
|
|
2
|
+
import { Skeleton } from "@src/components";
|
|
2
3
|
import { useWallet } from "@src/context/WalletProvider";
|
|
3
4
|
import { useSwapContext } from "@src/context";
|
|
4
5
|
import { SwapPanelType } from "../..";
|
|
@@ -21,8 +22,12 @@ export const WalletPanel = ({ chain, className }: Props) => {
|
|
|
21
22
|
solana,
|
|
22
23
|
sui,
|
|
23
24
|
} = useWallet();
|
|
24
|
-
const {
|
|
25
|
-
|
|
25
|
+
const {
|
|
26
|
+
integrationConfig,
|
|
27
|
+
hasOnConnectWalletCallback,
|
|
28
|
+
onConnectWallet,
|
|
29
|
+
isAsyncDataLoaded,
|
|
30
|
+
} = useSwapContext();
|
|
26
31
|
|
|
27
32
|
const openEvmWalletModal = useCallback(() => {
|
|
28
33
|
// Mirror SwapButton's contract: when the integrator supplied their own
|
|
@@ -68,6 +73,10 @@ export const WalletPanel = ({ chain, className }: Props) => {
|
|
|
68
73
|
return "";
|
|
69
74
|
}, [chain, evm, solana, sui]);
|
|
70
75
|
|
|
76
|
+
if (!isAsyncDataLoaded) {
|
|
77
|
+
return <Skeleton className="w-16 h-6" />;
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
// Reserve the button's height before the chain loads so the card
|
|
72
81
|
// doesn't grow when the wallet button appears.
|
|
73
82
|
if (!chain) {
|
|
@@ -85,7 +94,9 @@ export const WalletPanel = ({ chain, className }: Props) => {
|
|
|
85
94
|
<p className="text-[10px] leading-4 tracking-[0.01em] text-t_text_primary whitespace-nowrap">
|
|
86
95
|
{address ? shortAddress(address) : "Connect"}
|
|
87
96
|
</p>
|
|
88
|
-
|
|
97
|
+
{/* The 16px glyph has 6px of built-in whitespace on each side; pull the
|
|
98
|
+
right side back so the button's visual padding stays symmetric. */}
|
|
99
|
+
<div className="flex items-center justify-center w-4 h-4 -mr-1.5 text-t_text_primary opacity-50">
|
|
89
100
|
<ChevronDownThinSmallIcon />
|
|
90
101
|
</div>
|
|
91
102
|
</button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useSwapContext } from "@src/context";
|
|
2
|
-
import { useMemo } from "react";
|
|
2
|
+
import { MouseEvent, useMemo } from "react";
|
|
3
3
|
import { SwapPanelType } from "../index";
|
|
4
4
|
import { AmountPanel } from "./AmountPanel";
|
|
5
5
|
import { Balance } from "./AmountPanel/Balance";
|
|
@@ -23,11 +23,23 @@ export const SwapPanel = ({ type }: Props) => {
|
|
|
23
23
|
[type, srcChain, dstChain],
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
+
// The whole source card acts as the amount input's hit area; clicks on the
|
|
27
|
+
// nested buttons (wallet, MAX, token picker) must keep their own actions.
|
|
28
|
+
const focusAmountInput = (event: MouseEvent<HTMLDivElement>) => {
|
|
29
|
+
if (type !== "source" || (event.target as HTMLElement).closest("button")) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
event.currentTarget
|
|
33
|
+
.querySelector<HTMLInputElement>("#swap-amount-input")
|
|
34
|
+
?.focus();
|
|
35
|
+
};
|
|
36
|
+
|
|
26
37
|
return (
|
|
27
38
|
<div
|
|
39
|
+
onClick={focusAmountInput}
|
|
28
40
|
className={`flex flex-col gap-2 px-4 sm:px-6 py-4 rounded-2xl backdrop-blur-[12px] ${
|
|
29
41
|
type === "source"
|
|
30
|
-
? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5
|
|
42
|
+
? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5 swap-panel-gradient-border cursor-text"
|
|
31
43
|
: "bg-gradient-to-r from-t_bg_tertiary/[0.03] to-t_bg_tertiary/[0.03]"
|
|
32
44
|
}`}
|
|
33
45
|
>
|
|
@@ -167,7 +167,7 @@ export const WalletPicker: FC<{
|
|
|
167
167
|
return (
|
|
168
168
|
<div className={`flex flex-col h-full min-h-0 ${className || ""}`}>
|
|
169
169
|
<div className="flex justify-between items-center mb-6">
|
|
170
|
-
<div className="text-2xl leading-8 tracking-[0.01em] text-t_text_primary">
|
|
170
|
+
<div className="text-2xl font-medium leading-8 tracking-[0.01em] text-t_text_primary">
|
|
171
171
|
Select Wallet
|
|
172
172
|
</div>
|
|
173
173
|
<button
|
|
@@ -12,13 +12,15 @@ const ToggleButton = ({ checked, onClick }: Props) => {
|
|
|
12
12
|
onClick={onClick}
|
|
13
13
|
className={`relative inline-flex items-center w-10 h-5 p-0.5 rounded-full cursor-pointer transition-opacity border-none bg-gradient-to-r ${
|
|
14
14
|
checked
|
|
15
|
-
? "from-
|
|
15
|
+
? "from-x_blue_light/25 to-x_blue_dark/25"
|
|
16
16
|
: "from-t_main_accent_light/20 to-t_main_accent_dark/20 opacity-50"
|
|
17
17
|
}`}
|
|
18
18
|
>
|
|
19
19
|
<span
|
|
20
|
-
className={`w-4 h-4 rounded-full transition-transform bg-gradient-to-r
|
|
21
|
-
checked
|
|
20
|
+
className={`w-4 h-4 rounded-full transition-transform bg-gradient-to-r ${
|
|
21
|
+
checked
|
|
22
|
+
? "translate-x-5 from-x_blue_light to-x_blue_dark"
|
|
23
|
+
: "translate-x-0 from-t_main_accent_light to-t_main_accent_dark"
|
|
22
24
|
}`}
|
|
23
25
|
></span>
|
|
24
26
|
</button>
|
|
@@ -38,12 +38,8 @@ export const TxModal = ({
|
|
|
38
38
|
onMouseDown={onBackdropClick}
|
|
39
39
|
>
|
|
40
40
|
<div
|
|
41
|
-
className={`relative rounded-[32px] p-6 sm:p-8 w-full min-w-[340px] overflow-auto max-w-
|
|
42
|
-
type === "
|
|
43
|
-
? "border-[#F44336]"
|
|
44
|
-
: type === "success"
|
|
45
|
-
? "border-[#6AE89B]"
|
|
46
|
-
: type === "warning"
|
|
41
|
+
className={`relative rounded-[32px] p-6 sm:p-8 w-full min-w-[340px] overflow-auto max-w-[var(--modal-width,588px)] backdrop-blur-[32px] bg-t_bg_secondary bg-opacity-75 border border-solid ${
|
|
42
|
+
type === "warning"
|
|
47
43
|
? "border-t_warning_dark"
|
|
48
44
|
: type === "waiting"
|
|
49
45
|
? "border-t_main_accent_dark"
|
|
@@ -43,6 +43,91 @@
|
|
|
43
43
|
@apply border border-solid border-t_text_primary border-opacity-10;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/* 1px gradient stroke of the source swap panel (Figma 1762:39085): the
|
|
47
|
+
design's stroke is white 25%->50% stops under a 10% paint opacity, so the
|
|
48
|
+
effective colors are 2.5%->5% (hover: 50%->75% stops, i.e. 5%->7.5%).
|
|
49
|
+
Border-image can't round corners, hence the masked-pseudo-element ring;
|
|
50
|
+
the two pseudo-elements crossfade because gradients don't interpolate. */
|
|
51
|
+
.swap-panel-gradient-border {
|
|
52
|
+
position: relative;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.swap-panel-gradient-border::before,
|
|
56
|
+
.swap-panel-gradient-border::after {
|
|
57
|
+
content: "";
|
|
58
|
+
position: absolute;
|
|
59
|
+
inset: 0;
|
|
60
|
+
border-radius: inherit;
|
|
61
|
+
padding: 1px;
|
|
62
|
+
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
63
|
+
-webkit-mask-composite: xor;
|
|
64
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
65
|
+
mask-composite: exclude;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.swap-panel-gradient-border::before {
|
|
70
|
+
background: linear-gradient(
|
|
71
|
+
90deg,
|
|
72
|
+
color-mix(in srgb, var(--text-primary) 2.5%, transparent),
|
|
73
|
+
color-mix(in srgb, var(--text-primary) 5%, transparent)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.swap-panel-gradient-border::after {
|
|
78
|
+
background: linear-gradient(
|
|
79
|
+
90deg,
|
|
80
|
+
color-mix(in srgb, var(--text-primary) 5%, transparent),
|
|
81
|
+
color-mix(in srgb, var(--text-primary) 7.5%, transparent)
|
|
82
|
+
);
|
|
83
|
+
opacity: 0;
|
|
84
|
+
transition: opacity 0.15s ease;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.swap-panel-gradient-border:hover::after {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Surface of the network filter dropdown (Figma 1770:15593): a 20% black
|
|
92
|
+
wash over a subtle vertical sheen (white 4% -> 2% -> 4%).
|
|
93
|
+
The design carries those over a 72px background blur, but Chromium
|
|
94
|
+
ignores backdrop-filter nested inside the widget's own backdrop-blur,
|
|
95
|
+
so the translucent stack alone would let the token list read through.
|
|
96
|
+
Hence the opaque base underneath — it lands on the same near-black the
|
|
97
|
+
blurred design resolves to. */
|
|
98
|
+
.network-dropdown-surface {
|
|
99
|
+
background:
|
|
100
|
+
linear-gradient(
|
|
101
|
+
90deg,
|
|
102
|
+
color-mix(in srgb, var(--bg-primary) 20%, transparent) 0%,
|
|
103
|
+
color-mix(in srgb, var(--bg-primary) 20%, transparent) 100%
|
|
104
|
+
),
|
|
105
|
+
linear-gradient(
|
|
106
|
+
180deg,
|
|
107
|
+
color-mix(in srgb, var(--bg-tertiary) 4%, transparent) 0%,
|
|
108
|
+
color-mix(in srgb, var(--bg-tertiary) 2%, transparent) 49.5%,
|
|
109
|
+
color-mix(in srgb, var(--bg-tertiary) 4%, transparent) 100%
|
|
110
|
+
),
|
|
111
|
+
var(--bg-secondary);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Hover/selected pill of a network row ("Active Fill", Figma 1770:15599):
|
|
115
|
+
a horizontal sheen peaking in the middle and fading out at both ends.
|
|
116
|
+
The design's stops are white 0 -> 10% -> 50% -> 10% -> 0 under a 5% layer
|
|
117
|
+
opacity, so the effective values are the ones below. Gradients don't
|
|
118
|
+
interpolate, hence no transition. */
|
|
119
|
+
.network-dropdown-row:hover,
|
|
120
|
+
.network-dropdown-row-active {
|
|
121
|
+
background: linear-gradient(
|
|
122
|
+
90deg,
|
|
123
|
+
transparent 0%,
|
|
124
|
+
color-mix(in srgb, var(--bg-tertiary) 0.5%, transparent) 15%,
|
|
125
|
+
color-mix(in srgb, var(--bg-tertiary) 2.5%, transparent) 50%,
|
|
126
|
+
color-mix(in srgb, var(--bg-tertiary) 0.5%, transparent) 85%,
|
|
127
|
+
transparent 100%
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
46
131
|
/* Corner glow of the transaction result modal, fading towards the center */
|
|
47
132
|
.tx-modal-glow-success {
|
|
48
133
|
background: radial-gradient(
|
package/src/config/fonts.ts
CHANGED
|
@@ -52,9 +52,18 @@
|
|
|
52
52
|
// font-style: italic;
|
|
53
53
|
// }`;
|
|
54
54
|
|
|
55
|
+
import SatoshiRegularWOFF2 from "../fonts/Satoshi-Regular.woff2";
|
|
56
|
+
const SatoshiRegular = `@font-face {
|
|
57
|
+
font-family: "Satoshi";
|
|
58
|
+
src: url(${SatoshiRegularWOFF2}) format("woff2");
|
|
59
|
+
font-weight: 400;
|
|
60
|
+
font-display: swap;
|
|
61
|
+
font-style: normal;
|
|
62
|
+
}`;
|
|
63
|
+
|
|
55
64
|
import SatoshiMediumWOFF2 from "../fonts/Satoshi-Medium.woff2";
|
|
56
65
|
const SatoshiMedium = `@font-face {
|
|
57
|
-
font-family: "Satoshi
|
|
66
|
+
font-family: "Satoshi";
|
|
58
67
|
src: url(${SatoshiMediumWOFF2}) format("woff2");
|
|
59
68
|
font-weight: 500;
|
|
60
69
|
font-display: swap;
|
|
@@ -109,6 +118,10 @@ const SatoshiMedium = `@font-face {
|
|
|
109
118
|
// Fonts are inlined into JS bundle when we import them and they are added into DOM when we add them to this array.
|
|
110
119
|
// Use only fonts that you need.
|
|
111
120
|
// We don't want to increase the bundle size unnecessarily.
|
|
112
|
-
|
|
121
|
+
//
|
|
122
|
+
// Regular and Medium are registered under one "Satoshi" family so that
|
|
123
|
+
// `font-weight` picks a face. With a single face loaded every weight renders
|
|
124
|
+
// as that face, which is why `font-medium` used to be a no-op.
|
|
125
|
+
const enabledFonts = [SatoshiRegular, SatoshiMedium];
|
|
113
126
|
|
|
114
127
|
export const fontDefinitions = enabledFonts.join("\n");
|
|
@@ -498,14 +498,29 @@ export const SwapProvider = ({
|
|
|
498
498
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
499
499
|
[route],
|
|
500
500
|
);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
501
|
+
/**
|
|
502
|
+
* Whether the quote in hand is delivered as express delivery.
|
|
503
|
+
*
|
|
504
|
+
* The API states it on the route wherever the fee cannot: a CCTP transfer charges
|
|
505
|
+
* no express-delivery fee (Circle's fast-transfer fee comes out of the bridged
|
|
506
|
+
* USDC), so it reports the flag outright. That answer wins whenever it is there.
|
|
507
|
+
*
|
|
508
|
+
* Everywhere else the fee IS the signal — on the CCIP paths express delivery is
|
|
509
|
+
* what the non-zero express-delivery fee buys — so the fee test stays as the
|
|
510
|
+
* fallback.
|
|
511
|
+
*/
|
|
512
|
+
const isExpressDeliveryActive = useMemo(() => {
|
|
513
|
+
if (!route) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
if (typeof route.expressDelivery === "boolean") {
|
|
517
|
+
return route.expressDelivery;
|
|
518
|
+
}
|
|
519
|
+
return (
|
|
504
520
|
expressDelivery &&
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
);
|
|
521
|
+
BigNumber.from(route.xSwapFees?.expressDeliveryFee || "0").gt("0")
|
|
522
|
+
);
|
|
523
|
+
}, [expressDelivery, route]);
|
|
509
524
|
// useMemo: indicates whether asynchronous data is loaded based on the presence of supported chains.
|
|
510
525
|
const isAsyncDataLoaded = useMemo(
|
|
511
526
|
() => supportedChains.length > 0 && bridgeTokens.length > 0,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "@src/components";
|
|
9
9
|
import { ConfirmationView } from "@src/components/Swap/SwapView/ConfirmationView";
|
|
10
10
|
import { CCIP_EXPLORER } from "@src/constants";
|
|
11
|
+
import { useCrossChainDelivery } from "@src/hooks";
|
|
11
12
|
import {
|
|
12
13
|
Ecosystem,
|
|
13
14
|
EnqueueTxProps,
|
|
@@ -63,7 +64,33 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
63
64
|
const [txNeedsApproval, setTxNeedsApproval] = useState(false);
|
|
64
65
|
const [txStatus, setTxStatus] = useState(TxStatus.SWAP_INIT);
|
|
65
66
|
|
|
66
|
-
const { supportedChains, setSrcValue } = useSwapContext();
|
|
67
|
+
const { supportedChains, setSrcValue, srcChain, dstChain } = useSwapContext();
|
|
68
|
+
|
|
69
|
+
// A cross-chain swap is only done when the funds land on the destination, so once
|
|
70
|
+
// the source tx is mined the transfer is followed to its end rather than declared
|
|
71
|
+
// finished (the poll is also what claims a CCTP transfer — see the hook). Tracked
|
|
72
|
+
// here, not in the view, so the modal's own styling can follow it too.
|
|
73
|
+
const isCrossChainSwap =
|
|
74
|
+
!!srcChain && !!dstChain && srcChain.chainId !== dstChain.chainId;
|
|
75
|
+
const delivery = useCrossChainDelivery({
|
|
76
|
+
enabled:
|
|
77
|
+
txStatus === TxStatus.COMPLETED &&
|
|
78
|
+
!txError &&
|
|
79
|
+
// Only while the modal is up: the chains below are the ones currently
|
|
80
|
+
// selected, and the user is free to pick different ones the moment they close
|
|
81
|
+
// it. An unclaimed CCTP transfer is the executor's and the sweep's job from
|
|
82
|
+
// there on, not this poll's.
|
|
83
|
+
isTxModalOpen &&
|
|
84
|
+
// A custom message means the modal is running a standalone transaction
|
|
85
|
+
// (an approval reset, say), not a swap with a destination leg.
|
|
86
|
+
!txMsg &&
|
|
87
|
+
isCrossChainSwap &&
|
|
88
|
+
// The status endpoint reads an EVM receipt; a Solana source has none.
|
|
89
|
+
srcChain?.ecosystem === Ecosystem.EVM,
|
|
90
|
+
chainId: srcChain?.chainId,
|
|
91
|
+
txHash,
|
|
92
|
+
walletAddress: evm.address,
|
|
93
|
+
});
|
|
67
94
|
|
|
68
95
|
const closeTransactionModal = (transactionKey: number) => {
|
|
69
96
|
if (txKey.current === transactionKey) {
|
|
@@ -435,14 +462,18 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
435
462
|
isTxModalOpenRef.current = flag;
|
|
436
463
|
}, []);
|
|
437
464
|
|
|
465
|
+
// Funds still in flight read as "waiting": the success glow belongs to the
|
|
466
|
+
// delivery, not to the source tx that started it.
|
|
438
467
|
const modalType: TxModalType = useMemo(
|
|
439
468
|
() =>
|
|
440
469
|
txError
|
|
441
470
|
? "error"
|
|
471
|
+
: delivery.isPolling
|
|
472
|
+
? "waiting"
|
|
442
473
|
: txStatus === TxStatus.COMPLETED
|
|
443
474
|
? "success"
|
|
444
475
|
: "neutral",
|
|
445
|
-
[txError, txStatus],
|
|
476
|
+
[txError, txStatus, delivery.isPolling],
|
|
446
477
|
);
|
|
447
478
|
|
|
448
479
|
const state = useMemo<TxUIWrapperState>(
|
|
@@ -487,11 +518,10 @@ export const TxUIWrapper = ({ children }: Props) => {
|
|
|
487
518
|
{isTxModalOpen && (
|
|
488
519
|
<TxModal
|
|
489
520
|
id="tx_modal"
|
|
490
|
-
className="max-w-x_modal"
|
|
491
521
|
type={modalType}
|
|
492
522
|
onCloseClick={closeTxModal}
|
|
493
523
|
>
|
|
494
|
-
<ConfirmationView onCloseClick={closeTxModal} />
|
|
524
|
+
<ConfirmationView onCloseClick={closeTxModal} delivery={delivery} />
|
|
495
525
|
</TxModal>
|
|
496
526
|
)}
|
|
497
527
|
{children}
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CrossChainDeliveryStatus,
|
|
3
|
+
TransactionStatusResponse,
|
|
4
|
+
} from "@src/models";
|
|
5
|
+
import { getTransactionStatus } from "@src/services";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
|
|
8
|
+
/** Nothing changes after these: either the funds landed or they never will. */
|
|
9
|
+
const TERMINAL_STATUSES: CrossChainDeliveryStatus[] = [
|
|
10
|
+
"DONE",
|
|
11
|
+
"PARTIALLY_FAILED",
|
|
12
|
+
"FAILED",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/** A fast transfer lands in ~20s, so poll often enough that the UI is not the
|
|
16
|
+
* slowest part of it. */
|
|
17
|
+
const POLL_INTERVAL_MS = 4000;
|
|
18
|
+
/** Past the point where the user is plainly waiting on finality, not on us. */
|
|
19
|
+
const SLOW_POLL_INTERVAL_MS = 15000;
|
|
20
|
+
const SLOW_POLL_AFTER_MS = 3 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
type Params = {
|
|
23
|
+
/** Poll only while this is true — set it false for anything the endpoint cannot
|
|
24
|
+
* answer for (single-chain swaps, a non-EVM source, a tx that is not mined yet). */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Source chain of the tx, i.e. the chain `txHash` was mined on. */
|
|
27
|
+
chainId?: string;
|
|
28
|
+
txHash?: string;
|
|
29
|
+
walletAddress?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CrossChainDelivery = {
|
|
33
|
+
status: CrossChainDeliveryStatus | undefined;
|
|
34
|
+
response: TransactionStatusResponse | undefined;
|
|
35
|
+
/** Tracking this transfer and still waiting on it. */
|
|
36
|
+
isPolling: boolean;
|
|
37
|
+
isSettled: boolean;
|
|
38
|
+
isDelivered: boolean;
|
|
39
|
+
/** Delivered, but as the bridged token: the destination swap did not run. */
|
|
40
|
+
isPartiallyFailed: boolean;
|
|
41
|
+
isFailed: boolean;
|
|
42
|
+
/** When tracking began, for elapsed-time UI. Undefined while disabled. */
|
|
43
|
+
startedAt: number | undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Follows a submitted cross-chain transfer to its destination.
|
|
48
|
+
*
|
|
49
|
+
* The poll is not just an observer: the API claims an attested-but-unclaimed CCTP
|
|
50
|
+
* transfer from inside the same call, so polling is also what keeps the delivery
|
|
51
|
+
* moving. Which is the point — most transfers land in well under a minute, and a
|
|
52
|
+
* client that waits out a fixed 30-minute estimate instead of asking is both wrong
|
|
53
|
+
* and slower than the transfer it is describing.
|
|
54
|
+
*
|
|
55
|
+
* Polling stops as soon as the answer is terminal, and backs off to a slow cadence
|
|
56
|
+
* once the wait is long enough to be finality rather than latency.
|
|
57
|
+
*/
|
|
58
|
+
export const useCrossChainDelivery = ({
|
|
59
|
+
enabled,
|
|
60
|
+
chainId,
|
|
61
|
+
txHash,
|
|
62
|
+
walletAddress,
|
|
63
|
+
}: Params): CrossChainDelivery => {
|
|
64
|
+
const [response, setResponse] = useState<TransactionStatusResponse>();
|
|
65
|
+
const [startedAt, setStartedAt] = useState<number>();
|
|
66
|
+
|
|
67
|
+
const active = enabled && !!chainId && !!txHash;
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!active) {
|
|
71
|
+
setResponse(undefined);
|
|
72
|
+
setStartedAt(undefined);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const startTime = Date.now();
|
|
80
|
+
|
|
81
|
+
setResponse(undefined);
|
|
82
|
+
setStartedAt(startTime);
|
|
83
|
+
|
|
84
|
+
const poll = async () => {
|
|
85
|
+
try {
|
|
86
|
+
const next = await getTransactionStatus(
|
|
87
|
+
{
|
|
88
|
+
chainId: chainId as string,
|
|
89
|
+
txHash: txHash as string,
|
|
90
|
+
walletAddress,
|
|
91
|
+
},
|
|
92
|
+
controller.signal,
|
|
93
|
+
);
|
|
94
|
+
if (cancelled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setResponse(next);
|
|
98
|
+
if (TERMINAL_STATUSES.includes(next.status)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (cancelled) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// A single failed poll says nothing about the transfer — a cold RPC, a rate
|
|
106
|
+
// limit or a dropped request all look like this, and the next poll usually
|
|
107
|
+
// answers. Keep the loop alive and keep it out of the UI.
|
|
108
|
+
console.warn("Failed to read cross-chain delivery status:", err);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
timer = setTimeout(
|
|
112
|
+
poll,
|
|
113
|
+
Date.now() - startTime > SLOW_POLL_AFTER_MS
|
|
114
|
+
? SLOW_POLL_INTERVAL_MS
|
|
115
|
+
: POLL_INTERVAL_MS,
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
poll();
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
cancelled = true;
|
|
123
|
+
if (timer) {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
controller.abort();
|
|
127
|
+
};
|
|
128
|
+
}, [active, chainId, txHash, walletAddress]);
|
|
129
|
+
|
|
130
|
+
const status = response?.status;
|
|
131
|
+
const isSettled = !!status && TERMINAL_STATUSES.includes(status);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
status,
|
|
135
|
+
response,
|
|
136
|
+
isPolling: active && !isSettled,
|
|
137
|
+
isSettled,
|
|
138
|
+
isDelivered: status === "DONE",
|
|
139
|
+
isPartiallyFailed: status === "PARTIALLY_FAILED",
|
|
140
|
+
isFailed: status === "FAILED",
|
|
141
|
+
startedAt,
|
|
142
|
+
};
|
|
143
|
+
};
|
package/src/models/Route.ts
CHANGED
|
@@ -3,6 +3,19 @@ interface BaseRoute {
|
|
|
3
3
|
minAmountOut: string;
|
|
4
4
|
xSwapFees: XSwapFees;
|
|
5
5
|
message?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the route is delivered as express delivery, stated outright by the API.
|
|
8
|
+
*
|
|
9
|
+
* Served by the bridges whose speed the fee breakdown cannot express — today CCTP,
|
|
10
|
+
* which reports true on every route: it charges no express-delivery fee at all
|
|
11
|
+
* (Circle's fast-transfer fee comes out of the bridged USDC), so a client reading
|
|
12
|
+
* the fee saw every CCTP transfer as the slow tier.
|
|
13
|
+
*
|
|
14
|
+
* Absent everywhere else (the CCIP paths, single-chain), where the fee IS the
|
|
15
|
+
* signal — express delivery there is what a non-zero `expressDeliveryFee` buys —
|
|
16
|
+
* so fall back to the fee when this is undefined, never instead of it.
|
|
17
|
+
*/
|
|
18
|
+
expressDelivery?: boolean;
|
|
6
19
|
}
|
|
7
20
|
|
|
8
21
|
export interface EVMRoute extends BaseRoute {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* States the API reports for a submitted transaction (`GET /getTransactionStatus`).
|
|
3
|
+
*
|
|
4
|
+
* - `NOT_FOUND` — the source tx is not visible to the API's RPCs yet. Expected for
|
|
5
|
+
* the first poll or two after a swap; it is not a failure.
|
|
6
|
+
* - `IN_PROGRESS` — the source tx is mined and the destination has not settled yet.
|
|
7
|
+
* - `DONE` — the destination leg ran and the user holds the token they asked for.
|
|
8
|
+
* - `PARTIALLY_FAILED` — the transfer arrived but its destination swap did not run
|
|
9
|
+
* (or produced less than the committed minimum), so the bridged token was
|
|
10
|
+
* forwarded to the receiver as-is. Terminal: no swap will happen later.
|
|
11
|
+
* - `FAILED` — the source tx itself reverted.
|
|
12
|
+
* - `STUCK` — mined, but the bridge has not moved it for long enough that it needs
|
|
13
|
+
* looking at rather than waiting on.
|
|
14
|
+
*/
|
|
15
|
+
export type CrossChainDeliveryStatus =
|
|
16
|
+
| "NOT_FOUND"
|
|
17
|
+
| "IN_PROGRESS"
|
|
18
|
+
| "DONE"
|
|
19
|
+
| "STUCK"
|
|
20
|
+
| "FAILED"
|
|
21
|
+
| "PARTIALLY_FAILED";
|
|
22
|
+
|
|
23
|
+
export type DeliveryTransferInfo = {
|
|
24
|
+
hash: string;
|
|
25
|
+
chainId: string;
|
|
26
|
+
/** Absent on the plain tx references (the middle hop), which carry no transfer. */
|
|
27
|
+
token?: string;
|
|
28
|
+
amount?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type TransactionStatusResponse = {
|
|
32
|
+
status: CrossChainDeliveryStatus;
|
|
33
|
+
integratorHash?: string;
|
|
34
|
+
/** The speed the transfer actually settled at, as resolved by the bridge itself. */
|
|
35
|
+
expressDelivery?: boolean;
|
|
36
|
+
receiver?: string;
|
|
37
|
+
sourceTx?: DeliveryTransferInfo;
|
|
38
|
+
middleTx?: DeliveryTransferInfo;
|
|
39
|
+
targetTx?: DeliveryTransferInfo;
|
|
40
|
+
};
|