@xswap-link/sdk 0.15.1 → 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 +10 -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 +89 -89
- package/dist/index.js +1018 -766
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +911 -659
- 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 +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 +89 -8
- package/src/components/Swap/SwapView/ConfirmationView/index.tsx +19 -4
- package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +11 -5
- package/src/context/SwapProvider.tsx +22 -7
- package/src/context/TxUIWrapper.tsx +34 -3
- 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
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { TxWidget } from "@xswap-link/sdk";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
|
|
4
|
+
type Token = { address: string; symbol: string; quickPick?: boolean };
|
|
5
|
+
type Chain = {
|
|
6
|
+
chainId: string;
|
|
7
|
+
displayName: string;
|
|
8
|
+
ecosystem: string;
|
|
9
|
+
web3Environment: string;
|
|
10
|
+
bridgeSupported: boolean;
|
|
11
|
+
swapSupported: boolean;
|
|
12
|
+
tokens: Token[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type Cfg = Record<string, string | boolean | undefined>;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API = "https://xswap.link/api";
|
|
18
|
+
|
|
19
|
+
const API_URLS = [
|
|
20
|
+
DEFAULT_API,
|
|
21
|
+
"https://develop-72sp7lmyaa-ew.a.run.app/api/beta",
|
|
22
|
+
"http://localhost:3000/api",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const FONTS = ["Inter", "Roboto", "Poppins", "monospace", "serif"];
|
|
26
|
+
|
|
27
|
+
const BOOL_FIELDS = [
|
|
28
|
+
"lightTheme",
|
|
29
|
+
"defaultWalletPicker",
|
|
30
|
+
"bridge",
|
|
31
|
+
"srcChainLocked",
|
|
32
|
+
"srcTokenLocked",
|
|
33
|
+
"dstChainLocked",
|
|
34
|
+
"dstTokenLocked",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DEFAULTS: Cfg = {
|
|
38
|
+
integratorId: "d6e438dfa14e80701b9b",
|
|
39
|
+
defaultWalletPicker: true,
|
|
40
|
+
bridge: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const STORAGE_KEY = "xswap-example-config";
|
|
44
|
+
|
|
45
|
+
const loadCfg = (): Cfg => {
|
|
46
|
+
try {
|
|
47
|
+
return { ...DEFAULTS, ...JSON.parse(localStorage.getItem(STORAGE_KEY)!) };
|
|
48
|
+
} catch {
|
|
49
|
+
return DEFAULTS;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const str = (v: Cfg[string]) =>
|
|
54
|
+
typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
55
|
+
|
|
56
|
+
const log =
|
|
57
|
+
(name: string) =>
|
|
58
|
+
(...args: unknown[]) =>
|
|
59
|
+
console.log(`[${name}]`, ...args);
|
|
60
|
+
|
|
61
|
+
const toProps = (c: Cfg) => ({
|
|
62
|
+
integratorId: String(c.integratorId ?? ""),
|
|
63
|
+
srcChain: str(c.srcChain),
|
|
64
|
+
srcToken: str(c.srcToken),
|
|
65
|
+
dstChain: str(c.dstChain),
|
|
66
|
+
dstToken: str(c.dstToken),
|
|
67
|
+
dstDisplayToken: str(c.dstDisplayToken),
|
|
68
|
+
highlightedDstTokens: str(c.highlightedDstTokens)?.split(","),
|
|
69
|
+
widgetTitle: str(c.widgetTitle),
|
|
70
|
+
desc: str(c.desc),
|
|
71
|
+
integratorFee: str(c.integratorFee) ? Number(c.integratorFee) : undefined,
|
|
72
|
+
integratorFeeReceiverAddress: str(c.integratorFeeReceiverAddress),
|
|
73
|
+
override: str(c.apiUrl) ? { apiUrl: String(c.apiUrl) } : undefined,
|
|
74
|
+
styles: {
|
|
75
|
+
mainAccentLight: str(c.mainAccentLight),
|
|
76
|
+
mainAccentDark: str(c.mainAccentDark),
|
|
77
|
+
textSecondary: str(c.textSecondary),
|
|
78
|
+
fontFamily: str(c.fontFamily),
|
|
79
|
+
width: str(c.width),
|
|
80
|
+
},
|
|
81
|
+
lightTheme: !!c.lightTheme,
|
|
82
|
+
defaultWalletPicker: !!c.defaultWalletPicker,
|
|
83
|
+
bridge: !!c.bridge,
|
|
84
|
+
srcChainLocked: !!c.srcChainLocked,
|
|
85
|
+
srcTokenLocked: !!c.srcTokenLocked,
|
|
86
|
+
dstChainLocked: !!c.dstChainLocked,
|
|
87
|
+
dstTokenLocked: !!c.dstTokenLocked,
|
|
88
|
+
onSrcChainChange: log("onSrcChainChange"),
|
|
89
|
+
onSrcTokenChange: log("onSrcTokenChange"),
|
|
90
|
+
onDstChainChange: log("onDstChainChange"),
|
|
91
|
+
onDstTokenChange: log("onDstTokenChange"),
|
|
92
|
+
onConnectWallet: log("onConnectWallet"),
|
|
93
|
+
onPendingTransactionsChange: log("onPendingTransactionsChange"),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const useChains = (apiUrl: string) => {
|
|
97
|
+
const [chains, setChains] = useState<Chain[]>([]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
fetch(`${apiUrl}/chains?data={}`)
|
|
101
|
+
.then((r) => r.json())
|
|
102
|
+
.then((all: Chain[]) =>
|
|
103
|
+
setChains(
|
|
104
|
+
all.filter(
|
|
105
|
+
(c) =>
|
|
106
|
+
c.web3Environment === "mainnet" &&
|
|
107
|
+
(c.bridgeSupported || c.swapSupported),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
.catch((e) => console.error("failed to load chains", e));
|
|
112
|
+
}, [apiUrl]);
|
|
113
|
+
|
|
114
|
+
return chains;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const sortTokens = (tokens: Token[] = []) =>
|
|
118
|
+
[...tokens].sort((a, b) => Number(!!b.quickPick) - Number(!!a.quickPick));
|
|
119
|
+
|
|
120
|
+
type FieldProps = {
|
|
121
|
+
k: string;
|
|
122
|
+
cfg: Cfg;
|
|
123
|
+
set: (key: string, value: string | boolean) => void;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ponytail: declared outside App, otherwise every render remounts the inputs and typing loses focus
|
|
127
|
+
const Text = ({ k, cfg, set, ...rest }: FieldProps & Record<string, unknown>) => (
|
|
128
|
+
<label>
|
|
129
|
+
<span>{k}</span>
|
|
130
|
+
<input
|
|
131
|
+
value={(cfg[k] as string) ?? ""}
|
|
132
|
+
onChange={(e) => set(k, e.target.value)}
|
|
133
|
+
{...rest}
|
|
134
|
+
/>
|
|
135
|
+
</label>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const Color = ({ k, cfg, set }: FieldProps) => (
|
|
139
|
+
<label>
|
|
140
|
+
<span>{k}</span>
|
|
141
|
+
<div className="inline">
|
|
142
|
+
<input
|
|
143
|
+
type="color"
|
|
144
|
+
value={(cfg[k] as string) || "#3b82f6"}
|
|
145
|
+
onChange={(e) => set(k, e.target.value)}
|
|
146
|
+
/>
|
|
147
|
+
<input
|
|
148
|
+
value={(cfg[k] as string) ?? ""}
|
|
149
|
+
placeholder="unset"
|
|
150
|
+
onChange={(e) => set(k, e.target.value)}
|
|
151
|
+
/>
|
|
152
|
+
<button onClick={() => set(k, "")}>×</button>
|
|
153
|
+
</div>
|
|
154
|
+
</label>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const ChainSelect = ({
|
|
158
|
+
side,
|
|
159
|
+
cfg,
|
|
160
|
+
chains,
|
|
161
|
+
onChange,
|
|
162
|
+
}: {
|
|
163
|
+
side: "src" | "dst";
|
|
164
|
+
cfg: Cfg;
|
|
165
|
+
chains: Chain[];
|
|
166
|
+
onChange: (chainId: string) => void;
|
|
167
|
+
}) => (
|
|
168
|
+
<label>
|
|
169
|
+
<span>{side}Chain</span>
|
|
170
|
+
<select
|
|
171
|
+
value={(cfg[`${side}Chain`] as string) ?? ""}
|
|
172
|
+
onChange={(e) => onChange(e.target.value)}
|
|
173
|
+
>
|
|
174
|
+
<option value="">— any —</option>
|
|
175
|
+
{chains.map((c) => (
|
|
176
|
+
<option key={c.chainId} value={c.chainId}>
|
|
177
|
+
{c.displayName} ({c.ecosystem} · {c.chainId})
|
|
178
|
+
</option>
|
|
179
|
+
))}
|
|
180
|
+
</select>
|
|
181
|
+
</label>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const TokenSelect = ({
|
|
185
|
+
k,
|
|
186
|
+
cfg,
|
|
187
|
+
set,
|
|
188
|
+
tokens,
|
|
189
|
+
}: FieldProps & { tokens: Token[] }) => (
|
|
190
|
+
<label>
|
|
191
|
+
<span>{k}</span>
|
|
192
|
+
<select
|
|
193
|
+
value={(cfg[k] as string) ?? ""}
|
|
194
|
+
onChange={(e) => set(k, e.target.value)}
|
|
195
|
+
>
|
|
196
|
+
<option value="">— any —</option>
|
|
197
|
+
{tokens.map((t) => (
|
|
198
|
+
<option key={t.address} value={t.address}>
|
|
199
|
+
{t.symbol}
|
|
200
|
+
</option>
|
|
201
|
+
))}
|
|
202
|
+
</select>
|
|
203
|
+
</label>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
export const App = () => {
|
|
207
|
+
const [cfg, setCfg] = useState(loadCfg);
|
|
208
|
+
const [mountKey, setMountKey] = useState(0);
|
|
209
|
+
|
|
210
|
+
const chains = useChains(str(cfg.apiUrl) ?? DEFAULT_API);
|
|
211
|
+
const byId = useMemo(
|
|
212
|
+
() => Object.fromEntries(chains.map((c) => [c.chainId, c])),
|
|
213
|
+
[chains],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg));
|
|
218
|
+
}, [cfg]);
|
|
219
|
+
|
|
220
|
+
const set = (key: string, value: string | boolean) =>
|
|
221
|
+
setCfg((c) => ({ ...c, [key]: value }));
|
|
222
|
+
|
|
223
|
+
// ponytail: switching a chain invalidates the token picked on the old one
|
|
224
|
+
const setChain = (side: "src" | "dst", chainId: string) =>
|
|
225
|
+
setCfg((c) => ({
|
|
226
|
+
...c,
|
|
227
|
+
[`${side}Chain`]: chainId,
|
|
228
|
+
[`${side}Token`]: "",
|
|
229
|
+
...(side === "dst" ? { dstDisplayToken: "", highlightedDstTokens: "" } : {}),
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
const reset = () => {
|
|
233
|
+
setCfg(DEFAULTS);
|
|
234
|
+
setMountKey((k) => k + 1);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const srcTokens = sortTokens(byId[cfg.srcChain as string]?.tokens);
|
|
238
|
+
const dstTokens = sortTokens(byId[cfg.dstChain as string]?.tokens);
|
|
239
|
+
const highlighted = str(cfg.highlightedDstTokens)?.split(",") ?? [];
|
|
240
|
+
|
|
241
|
+
// ponytail: the widget reads chain/token props only on mount, so remount when they change.
|
|
242
|
+
// Text fields (integratorId, apiUrl) are excluded — they'd remount on every keystroke.
|
|
243
|
+
const remountKey = [
|
|
244
|
+
mountKey,
|
|
245
|
+
cfg.srcChain,
|
|
246
|
+
cfg.srcToken,
|
|
247
|
+
cfg.dstChain,
|
|
248
|
+
cfg.dstToken,
|
|
249
|
+
cfg.dstDisplayToken,
|
|
250
|
+
cfg.highlightedDstTokens,
|
|
251
|
+
].join("|");
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className="page">
|
|
255
|
+
<style>{CSS}</style>
|
|
256
|
+
|
|
257
|
+
<aside>
|
|
258
|
+
<h2>Widget config</h2>
|
|
259
|
+
|
|
260
|
+
<Text k="integratorId" cfg={cfg} set={set} />
|
|
261
|
+
|
|
262
|
+
<ChainSelect
|
|
263
|
+
side="src"
|
|
264
|
+
cfg={cfg}
|
|
265
|
+
chains={chains}
|
|
266
|
+
onChange={(id) => setChain("src", id)}
|
|
267
|
+
/>
|
|
268
|
+
<TokenSelect k="srcToken" cfg={cfg} set={set} tokens={srcTokens} />
|
|
269
|
+
<ChainSelect
|
|
270
|
+
side="dst"
|
|
271
|
+
cfg={cfg}
|
|
272
|
+
chains={chains}
|
|
273
|
+
onChange={(id) => setChain("dst", id)}
|
|
274
|
+
/>
|
|
275
|
+
<TokenSelect k="dstToken" cfg={cfg} set={set} tokens={dstTokens} />
|
|
276
|
+
<TokenSelect
|
|
277
|
+
k="dstDisplayToken"
|
|
278
|
+
cfg={cfg}
|
|
279
|
+
set={set}
|
|
280
|
+
tokens={dstTokens}
|
|
281
|
+
/>
|
|
282
|
+
|
|
283
|
+
<label>
|
|
284
|
+
<span>highlightedDstTokens (ctrl/cmd + click)</span>
|
|
285
|
+
<select
|
|
286
|
+
multiple
|
|
287
|
+
size={5}
|
|
288
|
+
value={highlighted}
|
|
289
|
+
onChange={(e) =>
|
|
290
|
+
set(
|
|
291
|
+
"highlightedDstTokens",
|
|
292
|
+
[...e.target.selectedOptions].map((o) => o.value).join(","),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
>
|
|
296
|
+
{dstTokens.map((t) => (
|
|
297
|
+
<option key={t.address} value={t.address}>
|
|
298
|
+
{t.symbol}
|
|
299
|
+
</option>
|
|
300
|
+
))}
|
|
301
|
+
</select>
|
|
302
|
+
</label>
|
|
303
|
+
|
|
304
|
+
<Text k="widgetTitle" cfg={cfg} set={set} placeholder="Bridge" />
|
|
305
|
+
<Text k="desc" cfg={cfg} set={set} />
|
|
306
|
+
<Text
|
|
307
|
+
k="integratorFee"
|
|
308
|
+
cfg={cfg}
|
|
309
|
+
set={set}
|
|
310
|
+
type="number"
|
|
311
|
+
step="0.001"
|
|
312
|
+
placeholder="0.003"
|
|
313
|
+
/>
|
|
314
|
+
<Text
|
|
315
|
+
k="integratorFeeReceiverAddress"
|
|
316
|
+
cfg={cfg}
|
|
317
|
+
set={set}
|
|
318
|
+
placeholder="0x…"
|
|
319
|
+
/>
|
|
320
|
+
|
|
321
|
+
<label>
|
|
322
|
+
<span>apiUrl (override)</span>
|
|
323
|
+
<input
|
|
324
|
+
list="api-urls"
|
|
325
|
+
value={(cfg.apiUrl as string) ?? ""}
|
|
326
|
+
placeholder={DEFAULT_API}
|
|
327
|
+
onChange={(e) => set("apiUrl", e.target.value)}
|
|
328
|
+
/>
|
|
329
|
+
<datalist id="api-urls">
|
|
330
|
+
{API_URLS.map((u) => (
|
|
331
|
+
<option key={u} value={u} />
|
|
332
|
+
))}
|
|
333
|
+
</datalist>
|
|
334
|
+
</label>
|
|
335
|
+
|
|
336
|
+
<h3>styles</h3>
|
|
337
|
+
<Color k="mainAccentLight" cfg={cfg} set={set} />
|
|
338
|
+
<Color k="mainAccentDark" cfg={cfg} set={set} />
|
|
339
|
+
<Color k="textSecondary" cfg={cfg} set={set} />
|
|
340
|
+
|
|
341
|
+
<label>
|
|
342
|
+
<span>fontFamily</span>
|
|
343
|
+
<input
|
|
344
|
+
list="fonts"
|
|
345
|
+
value={(cfg.fontFamily as string) ?? ""}
|
|
346
|
+
onChange={(e) => set("fontFamily", e.target.value)}
|
|
347
|
+
/>
|
|
348
|
+
<datalist id="fonts">
|
|
349
|
+
{FONTS.map((f) => (
|
|
350
|
+
<option key={f} value={f} />
|
|
351
|
+
))}
|
|
352
|
+
</datalist>
|
|
353
|
+
</label>
|
|
354
|
+
|
|
355
|
+
<label>
|
|
356
|
+
<span>width — {(cfg.width as string) || "default"}</span>
|
|
357
|
+
<div className="inline">
|
|
358
|
+
<input
|
|
359
|
+
type="range"
|
|
360
|
+
min={300}
|
|
361
|
+
max={600}
|
|
362
|
+
step={10}
|
|
363
|
+
value={parseInt((cfg.width as string) || "420", 10)}
|
|
364
|
+
onChange={(e) => set("width", `${e.target.value}px`)}
|
|
365
|
+
/>
|
|
366
|
+
<button onClick={() => set("width", "")}>×</button>
|
|
367
|
+
</div>
|
|
368
|
+
</label>
|
|
369
|
+
|
|
370
|
+
<h3>flags</h3>
|
|
371
|
+
<div className="flags">
|
|
372
|
+
{BOOL_FIELDS.map((f) => (
|
|
373
|
+
<label key={f} className="check">
|
|
374
|
+
<input
|
|
375
|
+
type="checkbox"
|
|
376
|
+
checked={!!cfg[f]}
|
|
377
|
+
onChange={(e) => set(f, e.target.checked)}
|
|
378
|
+
/>
|
|
379
|
+
{f}
|
|
380
|
+
</label>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div className="inline">
|
|
385
|
+
<button onClick={() => setMountKey((k) => k + 1)}>Remount</button>
|
|
386
|
+
<button onClick={reset}>Reset</button>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<pre>{JSON.stringify(toProps(cfg), null, 2)}</pre>
|
|
390
|
+
</aside>
|
|
391
|
+
|
|
392
|
+
<main>
|
|
393
|
+
<TxWidget key={remountKey} {...toProps(cfg)} />
|
|
394
|
+
</main>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const CSS = `
|
|
400
|
+
body { margin: 0; font: 13px/1.4 system-ui, sans-serif; background: #f6f7f9; }
|
|
401
|
+
.page { display: flex; gap: 32px; align-items: flex-start; padding: 24px; }
|
|
402
|
+
aside { width: 340px; display: flex; flex-direction: column; gap: 10px;
|
|
403
|
+
background: #fff; border: 1px solid #e3e5e8; border-radius: 12px; padding: 16px; }
|
|
404
|
+
main { position: sticky; top: 24px; }
|
|
405
|
+
h2, h3 { margin: 4px 0; }
|
|
406
|
+
h3 { color: #667; text-transform: uppercase; font-size: 11px; letter-spacing: .08em; }
|
|
407
|
+
label { display: flex; flex-direction: column; gap: 3px; }
|
|
408
|
+
label > span { color: #667; font-size: 11px; }
|
|
409
|
+
input, select, button { font: inherit; padding: 5px 6px; border: 1px solid #d3d6da;
|
|
410
|
+
border-radius: 6px; background: #fff; }
|
|
411
|
+
input[type=color] { padding: 0; width: 34px; height: 30px; }
|
|
412
|
+
input[type=range] { padding: 0; flex: 1; }
|
|
413
|
+
button { cursor: pointer; }
|
|
414
|
+
.inline { display: flex; gap: 6px; align-items: center; }
|
|
415
|
+
.inline > input:not([type=color]) { flex: 1; min-width: 0; }
|
|
416
|
+
.flags { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; }
|
|
417
|
+
.check { flex-direction: row; align-items: center; gap: 5px; font-size: 12px; }
|
|
418
|
+
.check input { margin: 0; }
|
|
419
|
+
pre { font-size: 10px; background: #f2f3f5; border-radius: 8px; padding: 10px;
|
|
420
|
+
max-height: 260px; overflow: auto; margin: 0; }
|
|
421
|
+
`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { App } from "./App";
|
|
4
|
+
|
|
5
|
+
// ponytail: SDK deps (solana/sui) expect a Buffer global in the browser
|
|
6
|
+
globalThis.Buffer ??= Buffer;
|
|
7
|
+
|
|
8
|
+
createRoot(document.getElementById("root")!).render(<App />);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
// ponytail: linked package -> force pre-bundling so its deps resolve from ../node_modules
|
|
7
|
+
optimizeDeps: { include: ["@xswap-link/sdk"] },
|
|
8
|
+
define: { global: "globalThis" },
|
|
9
|
+
});
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|