@xswap-link/sdk 0.2.5 → 0.3.0

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 (47) hide show
  1. package/.github/workflows/main.yml +2 -2
  2. package/.github/workflows/publish.yml +2 -2
  3. package/CHANGELOG.md +12 -0
  4. package/dist/index.css +325 -257
  5. package/dist/index.d.mts +4 -2
  6. package/dist/index.d.ts +4 -2
  7. package/dist/index.js +1296 -516
  8. package/dist/index.mjs +1323 -537
  9. package/package.json +1 -1
  10. package/src/components/Alert/index.tsx +1 -1
  11. package/src/components/Skeleton/index.tsx +3 -1
  12. package/src/components/Spinner/index.tsx +28 -0
  13. package/src/components/TxConfigForm/BalanceComponent.tsx +1 -1
  14. package/src/components/TxConfigForm/Button.tsx +1 -1
  15. package/src/components/TxConfigForm/ChainListElement.tsx +2 -2
  16. package/src/components/TxConfigForm/ConfirmationAmount.tsx +91 -0
  17. package/src/components/TxConfigForm/Description.tsx +5 -3
  18. package/src/components/TxConfigForm/ErrorField.tsx +4 -2
  19. package/src/components/TxConfigForm/FeesDetails.tsx +25 -31
  20. package/src/components/TxConfigForm/Form.tsx +763 -118
  21. package/src/components/TxConfigForm/History.tsx +5 -5
  22. package/src/components/TxConfigForm/HistoryCard.tsx +33 -40
  23. package/src/components/TxConfigForm/PoweredBy.tsx +2 -2
  24. package/src/components/TxConfigForm/Settings.tsx +33 -27
  25. package/src/components/TxConfigForm/Summary.tsx +33 -31
  26. package/src/components/TxConfigForm/SwapPanel.tsx +12 -15
  27. package/src/components/TxConfigForm/TokenPicker.tsx +36 -34
  28. package/src/components/TxConfigForm/TopBar.tsx +8 -8
  29. package/src/components/TxConfigForm/UsdPrice.tsx +4 -14
  30. package/src/components/TxConfigForm/index.tsx +37 -10
  31. package/src/components/TxStatusButton/index.tsx +42 -28
  32. package/src/components/UnknownTokenLogo/UnknownTokenLogo.tsx +1 -1
  33. package/src/components/global.css +7 -5
  34. package/src/components/icons/CircularProgressIcon.tsx +1 -1
  35. package/src/components/icons/ErrorIcon.tsx +32 -0
  36. package/src/components/icons/SignIcon.tsx +17 -0
  37. package/src/components/icons/SuccessIcon.tsx +29 -0
  38. package/src/components/icons/index.ts +3 -0
  39. package/src/config/init.tsx +42 -24
  40. package/src/constants/index.ts +2 -2
  41. package/src/models/payloads/GetSwapTxPayload.ts +2 -0
  42. package/src/services/integrations/monitoring.ts +10 -3
  43. package/src/services/integrations/transactions.ts +9 -3
  44. package/src/utils/contracts.ts +18 -0
  45. package/src/utils/index.ts +1 -0
  46. package/src/utils/parseWeb3Error.ts +93 -0
  47. package/tailwind.config.js +40 -0
@@ -1,11 +1,24 @@
1
1
  import "@src/global.css";
2
- import React, { FormEvent, useEffect, useState } from "react";
2
+ import {
3
+ FormEvent,
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
3
10
  import { ethers } from "ethers";
4
- import { useAccount, useSwitchChain } from "wagmi";
11
+ import { Config, useAccount, useSendTransaction, useSwitchChain } from "wagmi";
5
12
  import { Chain, Route, Token, TokenBalances, TokenPrices } from "@src/models";
6
13
  import { getCustomTokenData, getPrices, getRoute } from "@src/services";
7
- import { getBalances } from "@src/utils";
8
- import { ROUTE_TIMEOUT_MS } from "@src/constants";
14
+ import {
15
+ getAllowanceOf,
16
+ getBalances,
17
+ parseWeb3Error,
18
+ safeBigNumberFrom,
19
+ weiToHumanReadable,
20
+ } from "@src/utils";
21
+ import { CCIP_EXPLORER, ROUTE_TIMEOUT_MS } from "@src/constants";
9
22
  import { TxConfigFormProps } from "@src/components";
10
23
  import { useDebounce } from "@src/hooks";
11
24
  import { ADDRESSES } from "@src/contracts";
@@ -19,6 +32,21 @@ import { ErrorField } from "./ErrorField";
19
32
  import { Description } from "./Description";
20
33
  import { DEFAULT_SOURCE_CHAIN_ID } from "@src/constants";
21
34
  import { Button } from "./Button";
35
+ import { getChainId, waitForTransactionReceipt } from "wagmi/actions";
36
+ import { renderTxStatus } from "@src/services";
37
+ import { ConfirmationAmount } from "./ConfirmationAmount";
38
+ import {
39
+ ArrowDownIcon,
40
+ ArrowRightIcon,
41
+ ArrowUpRightIcon,
42
+ CheckIcon,
43
+ CloseIcon,
44
+ ErrorIcon,
45
+ SignIcon,
46
+ SuccessIcon,
47
+ } from "../icons";
48
+ import { Spinner } from "../Spinner";
49
+ import { UnknownTokenLogo } from "../UnknownTokenLogo/UnknownTokenLogo";
22
50
 
23
51
  export const Form = ({
24
52
  integratorId,
@@ -27,15 +55,23 @@ export const Form = ({
27
55
  customContractCalls,
28
56
  desc,
29
57
  supportedChains,
58
+ retrunTransactions,
59
+ dstDisplayTokenAddr,
60
+ wagmiConfig,
30
61
  onSubmit,
31
62
  onClose,
63
+ setBackdropClickDisabled,
32
64
  }: TxConfigFormProps & {
65
+ wagmiConfig: Config;
66
+ retrunTransactions: boolean;
33
67
  onSubmit: (route: Route) => void;
34
68
  onClose: () => void;
69
+ setBackdropClickDisabled: (disabled: boolean) => void;
35
70
  }) => {
36
71
  const [signer, setSigner] = useState<string | undefined>();
37
72
  const [dstChain, setDstChain] = useState<Chain>();
38
73
  const [dstToken, setDstToken] = useState<Token>();
74
+ const [dstDisplayToken, setDstDisplayToken] = useState<Token>();
39
75
  const [srcChain, setSrcChain] = useState<Chain>();
40
76
  const [srcToken, setSrcToken] = useState<Token>();
41
77
  const [amount, setAmount] = useState<string>("");
@@ -50,17 +86,43 @@ export const Form = ({
50
86
  const [formError, setFormError] = useState("");
51
87
  const [slippage, setSlippage] = useState<string>("1.5");
52
88
  const [feesDetailsShown, setFeesDetailsShown] = useState(false);
53
- const [exceedsExpressDeliveryLimit, setExceedsExpressDeliveryLimit] =
54
- useState(false);
89
+ const [executingTransactions, setExecutingTransactions] = useState(false);
90
+ const [transactionSuccessHash, setTransactionSuccessHash] = useState<
91
+ string | undefined
92
+ >();
93
+
94
+ const [txLoading, setTxLoading] = useState(false);
95
+ const [approveTxLoading, setApproveTxLoading] = useState(false);
96
+ const [approveTxDone, setApproveTxDone] = useState(false);
97
+ const [transactionError, setTransactionError] = useState<string | null>(null);
98
+ const currentRouteRequestNonce = useRef<number>(0);
99
+
100
+ // Allowance section
101
+ const prevAllowanceValuesRef = useRef({
102
+ srcChainId: srcChain?.chainId,
103
+ srcTokenAddress: srcToken?.address,
104
+ routeTo: ethers.constants.AddressZero,
105
+ });
106
+
107
+ const currentAllowanceRequestNonce = useRef<number>(0);
108
+
109
+ const [isAllowanceLoading, setIsAllowanceLoading] = useState(false);
110
+ const [srcTokenAllowance, setSrcTokenAllowance] = useState("0");
111
+ // End of Allowance section
112
+
113
+ // Wagmi hook to execute transactions
114
+ const { sendTransaction } = useSendTransaction({ config: wagmiConfig });
55
115
 
56
116
  const debouncedAmount = useDebounce(amount, 1000);
57
117
  const account = useAccount();
58
118
  const { switchChainAsync } = useSwitchChain();
59
119
 
120
+ // set signer address
60
121
  useEffect(() => {
61
122
  setSigner(account.address);
62
123
  }, [account.address]);
63
124
 
125
+ // set source chain
64
126
  useEffect(() => {
65
127
  const defaultChain = supportedChains.find(
66
128
  ({ chainId }) => chainId === account.chainId?.toString(),
@@ -73,10 +135,12 @@ export const Form = ({
73
135
  );
74
136
  }, [account.chainId]);
75
137
 
138
+ // set destination chain
76
139
  useEffect(() => {
77
140
  setDstChain(supportedChains.find((chain) => chain.chainId === dstChainId));
78
141
  }, [supportedChains, dstChainId]);
79
142
 
143
+ // set destination token
80
144
  useEffect(() => {
81
145
  (async () => {
82
146
  if (!dstChain) {
@@ -95,57 +159,85 @@ export const Form = ({
95
159
  }
96
160
 
97
161
  setDstToken(dstTokenResult);
162
+
163
+ if (dstDisplayTokenAddr) {
164
+ let dstDisplayTokenResult = dstChain?.tokens.find(
165
+ (token) =>
166
+ token.address.toLowerCase() === dstDisplayTokenAddr.toLowerCase(),
167
+ );
168
+
169
+ if (!dstDisplayTokenResult) {
170
+ dstDisplayTokenResult = await getCustomTokenData(
171
+ dstChain,
172
+ dstDisplayTokenAddr.toLowerCase(),
173
+ );
174
+ }
175
+
176
+ setDstDisplayToken(dstDisplayTokenResult);
177
+ }
98
178
  })();
99
179
  }, [dstChain]);
100
180
 
181
+ // Handle fetching route
101
182
  useEffect(() => {
102
183
  if (
103
- integratorId &&
104
- srcChain &&
105
- srcToken &&
106
- dstChain &&
107
- debouncedAmount &&
108
- paymentToken &&
109
- slippage
184
+ !(
185
+ integratorId &&
186
+ srcChain &&
187
+ srcToken &&
188
+ dstChain &&
189
+ debouncedAmount &&
190
+ paymentToken &&
191
+ signer &&
192
+ slippage
193
+ )
110
194
  ) {
111
- setFormError("");
112
- const timeout = setTimeout(() => {
113
- setFormError("Getting Route is taking longer than usually");
114
- }, ROUTE_TIMEOUT_MS);
115
- setIsGettingRoute(true);
116
- setFeesDetailsShown(false);
117
- if (!srcToken.decimals)
118
- throw new Error(
119
- `Configuration error: selected token ($${srcToken.symbol} ${srcToken.name}) has unknown decimals param.`,
120
- );
121
- getRoute({
122
- integratorId,
123
- fromAmount: ethers.utils
124
- .parseUnits(debouncedAmount, srcToken.decimals)
125
- .toString(),
126
- fromAddress: signer || ADDRESSES[srcChain.chainId]!.FeeCollector,
127
- fromChain: srcChain.chainId,
128
- fromToken: srcToken.address,
129
- toAddress: signer || ADDRESSES[dstChain.chainId]!.FeeCollector,
130
- toChain: dstChainId,
131
- toToken: dstTokenAddr,
132
- paymentToken: paymentToken.address,
133
- slippage: Number(slippage),
134
- expressDelivery: expressChecked && !exceedsExpressDeliveryLimit,
135
- customContractCalls,
136
- })
137
- .then((response) => {
138
- setRoute(response);
139
- setFormError("");
140
- })
141
- .catch((err) => {
142
- setFormError(err.message);
143
- })
144
- .finally(() => {
145
- setIsGettingRoute(false);
146
- clearTimeout(timeout);
147
- });
195
+ return;
148
196
  }
197
+
198
+ setFormError("");
199
+ const timeout = setTimeout(() => {
200
+ setFormError("Getting Route is taking longer than usually");
201
+ }, ROUTE_TIMEOUT_MS);
202
+ setIsGettingRoute(true);
203
+ setFeesDetailsShown(false);
204
+ if (!srcToken.decimals)
205
+ throw new Error(
206
+ `Configuration error: selected token ($${srcToken.symbol} ${srcToken.name}) has unknown decimals param.`,
207
+ );
208
+
209
+ const nonce = Date.now();
210
+ currentRouteRequestNonce.current = nonce;
211
+ getRoute({
212
+ integratorId,
213
+ fromAmount: ethers.utils
214
+ .parseUnits(debouncedAmount, srcToken.decimals)
215
+ .toString(),
216
+ fromAddress: signer || ADDRESSES[srcChain.chainId]!.FeeCollector,
217
+ fromChain: srcChain.chainId,
218
+ fromToken: srcToken.address,
219
+ toAddress: signer || ADDRESSES[dstChain.chainId]!.FeeCollector,
220
+ toChain: dstChainId,
221
+ toToken: dstTokenAddr,
222
+ paymentToken: paymentToken.address,
223
+ slippage: Number(slippage),
224
+ expressDelivery: expressChecked,
225
+ customContractCalls,
226
+ })
227
+ .then((response) => {
228
+ if (nonce !== currentRouteRequestNonce.current) {
229
+ return;
230
+ }
231
+ setRoute(response);
232
+ setFormError("");
233
+ })
234
+ .catch((err) => {
235
+ setFormError(err.message);
236
+ })
237
+ .finally(() => {
238
+ setIsGettingRoute(false);
239
+ clearTimeout(timeout);
240
+ });
149
241
  }, [
150
242
  signer,
151
243
  srcChain,
@@ -156,6 +248,58 @@ export const Form = ({
156
248
  expressChecked,
157
249
  ]);
158
250
 
251
+ // Handle fetching allowance
252
+ useEffect(() => {
253
+ (async () => {
254
+ if (
255
+ !signer ||
256
+ !srcToken?.address ||
257
+ !srcChain?.publicRpcUrls[0] ||
258
+ srcToken?.address === ethers.constants.AddressZero
259
+ ) {
260
+ return;
261
+ }
262
+ if (route && route.transactions.swap?.to) {
263
+ const {
264
+ srcChainId: prevFromChainId,
265
+ srcTokenAddress: prevFromTokenAddress,
266
+ routeTo: prevRouteTo,
267
+ } = prevAllowanceValuesRef.current;
268
+
269
+ const hasChanges =
270
+ srcChain?.chainId !== prevFromChainId ||
271
+ srcToken?.address !== prevFromTokenAddress ||
272
+ route.transactions.swap?.to !== prevRouteTo;
273
+
274
+ if (!hasChanges) {
275
+ return;
276
+ }
277
+
278
+ prevAllowanceValuesRef.current = {
279
+ srcChainId: srcChain?.chainId,
280
+ srcTokenAddress: srcToken?.address,
281
+ routeTo: route.transactions.swap?.to,
282
+ };
283
+
284
+ const nonce = Date.now();
285
+ currentAllowanceRequestNonce.current = nonce;
286
+ setIsAllowanceLoading(true);
287
+ const allowanceResponse = await getAllowanceOf(
288
+ srcToken?.address,
289
+ signer,
290
+ route.transactions.swap.to,
291
+ srcChain?.publicRpcUrls[0],
292
+ );
293
+ if (nonce !== currentAllowanceRequestNonce.current) {
294
+ return;
295
+ }
296
+ setSrcTokenAllowance(allowanceResponse);
297
+ setIsAllowanceLoading(false);
298
+ }
299
+ })();
300
+ }, [srcChain, srcToken?.address, signer, setSrcTokenAllowance, route]);
301
+
302
+ // Set fee payment token
159
303
  useEffect(() => {
160
304
  if (srcChain) {
161
305
  setPaymentToken(
@@ -168,15 +312,27 @@ export const Form = ({
168
312
  }
169
313
  }, [srcChain]);
170
314
 
315
+ // Clear state on source token change
171
316
  useEffect(() => {
172
317
  setAmount("");
173
318
  setFormError("");
319
+ setIsGettingRoute(false);
320
+ setRoute(undefined);
321
+ const nonce = Date.now();
322
+ currentRouteRequestNonce.current = nonce;
174
323
  }, [srcToken]);
175
324
 
325
+ // Clear state on source chain change
176
326
  useEffect(() => {
177
327
  setSrcToken(srcChain?.tokens[0]);
328
+ setAmount("");
329
+ setIsGettingRoute(false);
330
+ setRoute(undefined);
331
+ const nonce = Date.now();
332
+ currentRouteRequestNonce.current = nonce;
178
333
  }, [srcChain]);
179
334
 
335
+ // Fetch token prices on source chain
180
336
  useEffect(() => {
181
337
  if (srcChain) {
182
338
  getPrices({ chainId: srcChain.chainId, currency: "USD" }).then((prices) =>
@@ -185,94 +341,583 @@ export const Form = ({
185
341
  }
186
342
  }, [srcChain]);
187
343
 
344
+ // Fetch token balances on source chain
188
345
  useEffect(() => {
189
346
  if (srcChain && signer) {
190
347
  getBalances(srcChain, signer).then((balances) => setBalances(balances));
191
348
  }
192
349
  }, [srcChain, signer]);
193
350
 
351
+ // Disable backdrop clicks when executing transactions
352
+ useEffect(() => {
353
+ setBackdropClickDisabled(executingTransactions);
354
+ }, [executingTransactions]);
355
+
356
+ // Check if the allowance needs to be adjusted
357
+ const isAllowanceOk = useMemo(() => {
358
+ return (
359
+ srcToken?.address === ethers.constants.AddressZero ||
360
+ Number(srcTokenAllowance) >= Number(amount)
361
+ );
362
+ }, [srcToken, amount, srcTokenAllowance]);
363
+
364
+ // Execute the transactions recieved from the xPay.
365
+ const triggerTx = useCallback(async () => {
366
+ if (!route) {
367
+ return;
368
+ }
369
+ setExecutingTransactions(true);
370
+ setBackdropClickDisabled(true);
371
+ setTransactionError(null);
372
+
373
+ // Optional approve for ERC20 tokens.
374
+ if (route.transactions.approve && !isAllowanceOk) {
375
+ setApproveTxDone(false);
376
+
377
+ const { to, data } = route.transactions.approve;
378
+ await sendTransaction(
379
+ {
380
+ to,
381
+ data,
382
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
383
+ } as any,
384
+ {
385
+ onSuccess: async (hash) => {
386
+ setApproveTxLoading(true);
387
+ const chainId = getChainId(wagmiConfig);
388
+ await waitForTransactionReceipt(wagmiConfig, {
389
+ chainId,
390
+ hash,
391
+ });
392
+ setApproveTxLoading(false);
393
+ setApproveTxDone(true);
394
+ swap(route);
395
+ },
396
+ onError: (error) => {
397
+ setTransactionError(`Approval failed: ${error.message}`);
398
+ setApproveTxLoading(false);
399
+ setBackdropClickDisabled(false);
400
+ },
401
+ },
402
+ );
403
+ } else {
404
+ await swap(route);
405
+ }
406
+ }, [
407
+ route,
408
+ signer,
409
+ srcChain,
410
+ srcToken,
411
+ debouncedAmount,
412
+ paymentToken,
413
+ slippage,
414
+ expressChecked,
415
+ amount,
416
+ srcTokenAllowance,
417
+ ]);
418
+
419
+ // Sign and send the swap transaction
420
+ const swap = useCallback(
421
+ async (route: Route) => {
422
+ const { to, value, data } = route.transactions.swap;
423
+ await sendTransaction(
424
+ {
425
+ to,
426
+ value: BigInt(value),
427
+ data,
428
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
429
+ } as any,
430
+ {
431
+ onSuccess: async (hash) => {
432
+ setTxLoading(true);
433
+ // Open the transaction monitor popup to inform user about the tx status
434
+ const chainId = getChainId(wagmiConfig);
435
+ await waitForTransactionReceipt(wagmiConfig, {
436
+ chainId,
437
+ hash,
438
+ });
439
+ setTxLoading(false);
440
+ if (srcChain?.chainId !== dstChain?.chainId) {
441
+ renderTxStatus(chainId.toString(), hash);
442
+ }
443
+ setTransactionSuccessHash(hash);
444
+ // onSubmit(route);
445
+ },
446
+ onError: (error) => {
447
+ setBackdropClickDisabled(false);
448
+ setTransactionError(`Transaction failed: ${error.message}`);
449
+ },
450
+ },
451
+ );
452
+ },
453
+ [
454
+ route,
455
+ signer,
456
+ srcChain,
457
+ srcToken,
458
+ debouncedAmount,
459
+ paymentToken,
460
+ slippage,
461
+ expressChecked,
462
+ ],
463
+ );
464
+
465
+ // Handle form submit
194
466
  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
195
467
  event.preventDefault();
196
- if (route) onSubmit(route);
468
+ if (!route) {
469
+ return;
470
+ }
471
+
472
+ if (retrunTransactions) {
473
+ onSubmit(route);
474
+ return;
475
+ }
476
+
477
+ triggerTx();
197
478
  };
198
479
 
199
480
  const handleSwitchChain = async () => {
200
481
  await switchChainAsync({ chainId: Number(srcChain?.chainId) });
201
482
  };
202
483
 
484
+ // provides data for the main button
485
+ const swapButton: { text: string; fn: () => void; disabled: boolean } =
486
+ useMemo(() => {
487
+ let disabled = false;
488
+
489
+ if (transactionError) {
490
+ return {
491
+ text: transactionError,
492
+ fn: () => {},
493
+ disabled: true,
494
+ };
495
+ }
496
+
497
+ if (!signer) {
498
+ return {
499
+ text: "Connect wallet",
500
+ fn: () => {},
501
+ disabled,
502
+ };
503
+ }
504
+
505
+ if (
506
+ signer &&
507
+ srcChain &&
508
+ srcChain.chainId !== account.chainId?.toString()
509
+ ) {
510
+ return {
511
+ text: "Switch chain",
512
+ fn: handleSwitchChain,
513
+ disabled,
514
+ };
515
+ }
516
+
517
+ if (!amount) {
518
+ return {
519
+ text: `Enter token amount`,
520
+ fn: () => {},
521
+ disabled: true,
522
+ };
523
+ }
524
+
525
+ if (
526
+ amount &&
527
+ balances &&
528
+ srcToken &&
529
+ srcToken.decimals &&
530
+ balances[srcToken?.address] !== undefined &&
531
+ !ethers.utils
532
+ .parseUnits(amount, srcToken.decimals)
533
+ .lt(balances[srcToken?.address] || "0")
534
+ ) {
535
+ return {
536
+ text: "Insufficient balance",
537
+ fn: () => {},
538
+ disabled: true,
539
+ };
540
+ }
541
+
542
+ if (!route) {
543
+ return {
544
+ text: `Loading route`,
545
+ fn: () => {},
546
+ disabled: true,
547
+ };
548
+ }
549
+
550
+ if (isAllowanceLoading) {
551
+ return {
552
+ text: `Checking allowance`,
553
+ fn: () => {},
554
+ disabled: true,
555
+ };
556
+ }
557
+
558
+ return {
559
+ text: "Submit",
560
+ fn: () => {},
561
+ disabled,
562
+ };
563
+ }, [
564
+ account,
565
+ srcToken,
566
+ srcChain,
567
+ isAllowanceOk,
568
+ amount,
569
+ balances,
570
+ signer,
571
+ transactionError,
572
+ route,
573
+ isAllowanceLoading,
574
+ ]);
575
+
576
+ // check if the express delivery will be executed
577
+ const isExpressDelivery = useMemo(() => {
578
+ let expressDeliveryFee;
579
+ if (!route?.xSwapFees || !route?.xSwapFees.expressDeliveryFee) {
580
+ expressDeliveryFee = safeBigNumberFrom("0");
581
+ } else {
582
+ expressDeliveryFee = safeBigNumberFrom(
583
+ route?.xSwapFees.expressDeliveryFee,
584
+ );
585
+ }
586
+
587
+ if (expressDeliveryFee.gt("0")) {
588
+ return true;
589
+ }
590
+ return false;
591
+ }, [route]);
592
+
203
593
  return (
204
- <form
205
- className="flex flex-col gap-2 z-10 my-0 p-2 md:p-4 rounded-3xl overflow-hidden font-light sm:w-x_desktop"
206
- onSubmit={handleSubmit}
207
- >
208
- <TopBar
209
- signer={signer}
210
- historyTabShown={historyTabShown}
211
- setHistoryTabShown={setHistoryTabShown}
212
- setSettingsShown={setSettingsShown}
213
- onClose={onClose}
214
- />
215
- {settingsShown && (
216
- <Settings
217
- expressChecked={expressChecked}
218
- setExpressChecked={setExpressChecked}
219
- setSettingsShown={setSettingsShown}
220
- slippage={slippage}
221
- setSlippage={setSlippage}
222
- />
223
- )}
224
- {historyTabShown ? (
225
- <History signer={signer} supportedChains={supportedChains} />
594
+ <>
595
+ {!signer ? (
596
+ // Connect wallet modal
597
+ <div className="xpay-flex xpay-flex-col xpay-gap-2 xpay-z-10 xpay-my-0 xpay-p-2 md:xpay-p-4 xpay-rounded-3xl xpay-overflow-hidden xpay-font-light sm:xpay-w-x_desktop xpay-w-screen">
598
+ <div className="xpay-flex xpay-w-full xpay-justify-end xpay-items-center xpay-mx-auto xpay-p-2">
599
+ <div
600
+ className="xpay-cursor-pointer xpay-text-white"
601
+ onClick={onClose}
602
+ >
603
+ <CloseIcon />
604
+ </div>
605
+ </div>{" "}
606
+ <p className="xpay-text-xl xpay-font-bold xpay-leading-8 xpay-text-center xpay-text-white">
607
+ Please connect the wallet first.
608
+ </p>
609
+ </div>
610
+ ) : executingTransactions ? (
611
+ <div className="xpay-flex xpay-flex-col xpay-gap-2 xpay-z-10 xpay-my-0 xpay-p-2 md:xpay-p-4 xpay-rounded-3xl xpay-overflow-hidden xpay-font-light sm:xpay-w-x_desktop xpay-w-screen">
612
+ {transactionError ? (
613
+ // Tx Error modal
614
+ <div>
615
+ <div className="xpay-flex xpay-w-full xpay-justify-end xpay-items-center xpay-mx-auto xpay-p-2">
616
+ <div
617
+ className="xpay-cursor-pointer xpay-text-white"
618
+ onClick={onClose}
619
+ >
620
+ <CloseIcon />
621
+ </div>
622
+ </div>
623
+
624
+ <div className="xpay-flex xpay-flex-col xpay-w-full xpay-justify-center xpay-items-center xpay-mx-auto xpay-p-2 xpay-gap-4">
625
+ <div>
626
+ <ErrorIcon />
627
+ </div>
628
+
629
+ <div className="xpay-flex xpay-flex-col">
630
+ <div>
631
+ <p className="xpay-text-2xl xpay-font-bold xpay-leading-8 xpay-text-center xpay-text-white">
632
+ Transaction error!
633
+ </p>
634
+ </div>
635
+ <div>
636
+ <p className="xpay-text-sm xpay-text-center xpay-text-white xpay-opacity-50">
637
+ {parseWeb3Error(transactionError)}
638
+ </p>
639
+ </div>
640
+ </div>
641
+ </div>
642
+ </div>
643
+ ) : transactionSuccessHash ? (
644
+ // Tx Success modal
645
+ <div>
646
+ <div className="xpay-flex xpay-w-full xpay-justify-end xpay-items-center xpay-mx-auto xpay-p-2">
647
+ <div
648
+ className="xpay-cursor-pointer xpay-text-white"
649
+ onClick={onClose}
650
+ >
651
+ <CloseIcon />
652
+ </div>
653
+ </div>
654
+
655
+ <div className="xpay-flex xpay-flex-col xpay-w-full xpay-justify-center xpay-items-center xpay-mx-auto xpay-p-2 xpay-gap-4">
656
+ <div>
657
+ <SuccessIcon />
658
+ </div>
659
+ <div className="xpay-flex xpay-flex-col">
660
+ <div>
661
+ <p className="xpay-text-2xl xpay-font-bold xpay-leading-8 xpay-text-center xpay-text-white">
662
+ Transaction success!
663
+ </p>
664
+ </div>
665
+ {srcChain?.chainId !== dstChain?.chainId ? (
666
+ <div>
667
+ <p className="xpay-text-sm xpay-text-center xpay-text-white xpay-opacity-50">
668
+ Approximated time of delivery:{" "}
669
+ {isExpressDelivery ? "30 seconds" : "30 minutes"}
670
+ </p>
671
+ </div>
672
+ ) : (
673
+ <></>
674
+ )}
675
+ </div>
676
+
677
+ <div className="xpay-flex xpay-gap-4 xpay-items-center">
678
+ <div className="xpay-flex xpay-gap-2">
679
+ <div className="xpay-w-10 xpay-h-10">
680
+ {srcToken?.image ? (
681
+ <img
682
+ className="xpay-w-10 xpay-h-10"
683
+ src={srcToken?.image}
684
+ alt={srcToken?.name}
685
+ />
686
+ ) : (
687
+ <UnknownTokenLogo
688
+ tokenName={"?"}
689
+ className="xpay-w-10 xpay-h-10"
690
+ />
691
+ )}
692
+ </div>
693
+ <div className="xpay-flex xpay-flex-col xpay-items-start">
694
+ <div className="xpay-flex">
695
+ {`${debouncedAmount} ${srcToken?.symbol}`}
696
+ </div>
697
+ <div className="xpay-text-sm xpay-text-center xpay-text-white xpay-opacity-50">
698
+ {srcChain?.displayName}
699
+ </div>
700
+ </div>
701
+ </div>
702
+
703
+ <div className="xpay-w-3.5 xpay-h-3.5">
704
+ <ArrowRightIcon />
705
+ </div>
706
+
707
+ <div className="xpay-flex xpay-gap-2">
708
+ <div className="xpay-w-10 xpay-h-10">
709
+ {(dstDisplayToken ? dstDisplayToken : dstToken)?.image ? (
710
+ <img
711
+ className="xpay-w-10 xpay-h-10"
712
+ src={
713
+ (dstDisplayToken ? dstDisplayToken : dstToken)
714
+ ?.image
715
+ }
716
+ alt={
717
+ (dstDisplayToken ? dstDisplayToken : dstToken)?.name
718
+ }
719
+ />
720
+ ) : (
721
+ <UnknownTokenLogo
722
+ tokenName={"?"}
723
+ className="xpay-w-10 xpay-h-10"
724
+ />
725
+ )}
726
+ </div>
727
+ <div className="xpay-flex xpay-flex-col xpay-items-start">
728
+ <div className="xpay-flex">
729
+ {weiToHumanReadable({
730
+ amount: route?.estAmountOut || "0",
731
+ decimals:
732
+ (dstDisplayToken ? dstDisplayToken : dstToken)
733
+ ?.decimals || 18,
734
+ precisionFractionalPlaces: 4,
735
+ })}{" "}
736
+ {(dstDisplayToken ? dstDisplayToken : dstToken)?.symbol}
737
+ </div>
738
+ <div className="xpay-text-sm xpay-text-center xpay-text-white xpay-opacity-50">
739
+ {dstChain?.displayName}
740
+ </div>
741
+ </div>
742
+ </div>
743
+ </div>
744
+
745
+ <div>
746
+ <a
747
+ href={
748
+ srcChain?.chainId !== dstChain?.chainId
749
+ ? `${CCIP_EXPLORER}/tx/${transactionSuccessHash}`
750
+ : `${
751
+ supportedChains.find(
752
+ (chain) => chain.chainId === srcChain?.chainId,
753
+ )?.transactionExplorer
754
+ }/${transactionSuccessHash}`
755
+ }
756
+ target="_blank"
757
+ rel="noreferrer"
758
+ className="xpay-flex xpay-items-center xpay-gap-2 xpay-font-bold xpay-no-underline xpay-cursor-pointer xpay-text-center"
759
+ aria-label="Show the transaction in the chain explorer"
760
+ >
761
+ <p className="xpay-bg-gradient-to-r xpay-from-x_blue_light xpay-to-x_blue_dark xpay-bg-clip-text xpay-text-transparent xpay-underline">
762
+ View details on Explorer
763
+ </p>
764
+ <div className="xpay-w-3.5 xpay-h-3.5 xpay-bg-gradient-to-r xpay-text-x_blue_dark">
765
+ <ArrowUpRightIcon />
766
+ </div>
767
+ </a>
768
+ </div>
769
+ </div>
770
+ </div>
771
+ ) : (
772
+ // Tx Confirmation modal
773
+ <>
774
+ <div className="xpay-flex xpay-w-full xpay-justify-between xpay-items-center xpay-mx-auto xpay-p-2 xpay-font-light ">
775
+ <p className="xpay-text-white xpay-text-xl">Confirmation</p>
776
+ <div className="xpay-flex xpay-w-full xpay-justify-end xpay-items-center xpay-mx-auto xpay-p-2">
777
+ <div
778
+ className="xpay-cursor-pointer xpay-text-white"
779
+ onClick={onClose}
780
+ >
781
+ <CloseIcon />
782
+ </div>
783
+ </div>
784
+ </div>
785
+
786
+ <div className="xpay-flex xpay-flex-col xpay-gap-2">
787
+ <ConfirmationAmount
788
+ amount={ethers.utils
789
+ .parseUnits(debouncedAmount, srcToken?.decimals)
790
+ .toString()}
791
+ chain={srcChain}
792
+ token={srcToken}
793
+ type="src"
794
+ />
795
+ </div>
796
+
797
+ <div className="xpay-flex xpay-justify-center xpay-globalBorder xpay-rounded-xl xpay-p-[5px] xpay--mt-7 xpay--mb-6 ">
798
+ <div className="xpay-flex xpay-items-center xpay-justify-center xpay-rounded-lg xpay-w-8 xpay-h-8 xpay-bg-gradient-to-l xpay-from-[rgba(54,129,198,1)] xpay-to-[rgba(43,74,157,1)] ">
799
+ <ArrowDownIcon />
800
+ </div>
801
+ </div>
802
+
803
+ <div className="xpay-flex xpay-flex-col xpay-gap-2">
804
+ <ConfirmationAmount
805
+ amount={route?.estAmountOut || "0"}
806
+ chain={dstChain}
807
+ token={dstDisplayToken ? dstDisplayToken : dstToken}
808
+ type="dst"
809
+ />
810
+ </div>
811
+ <div className="xpay-flex xpay-flex-col xpay-globalBorder xpay-p-4 xpay-justify-center xpay-rounded-xl xpay-bg-[rgba(15,15,15,1)] xpay-relative">
812
+ {route?.transactions.approve && !isAllowanceOk && (
813
+ <>
814
+ <div className="xpay-flex xpay-justify-between xpay-items-center">
815
+ <div className="xpay-flex xpay-items-center xpay-gap-2">
816
+ <SignIcon />
817
+ <p className="xpay-opacity-50">
818
+ {approveTxLoading
819
+ ? "Wait for approval confirmation"
820
+ : "Approve token spending"}
821
+ </p>
822
+ </div>
823
+ <div className="xpay-flex xpay-items-center">
824
+ {approveTxLoading && <Spinner width="5" height="5" />}
825
+ {approveTxDone && (
826
+ <div className="xpay-text-x_green xpay-w-5 xpay-h-5">
827
+ <CheckIcon />
828
+ </div>
829
+ )}
830
+ </div>
831
+ </div>
832
+ <div className="xpay-w-px xpay-h-6 xpay-mt-1 xpay-mb-1 xpay-ml-2.5 xpay-bg-white xpay-opacity-50"></div>
833
+ </>
834
+ )}
835
+ <div className="xpay-flex xpay-justify-between xpay-items-center">
836
+ <div className="xpay-flex xpay-items-center xpay-gap-2">
837
+ <SignIcon />
838
+ <p className="xpay-opacity-50">
839
+ {txLoading
840
+ ? "Wait for transaction confirmation"
841
+ : "Confirm transaction"}
842
+ </p>
843
+ </div>
844
+ <div className="xpay-flex xpay-items-center">
845
+ {txLoading && <Spinner width="5" height="5" />}
846
+ </div>
847
+ </div>
848
+ </div>
849
+ </>
850
+ )}
851
+ </div>
226
852
  ) : (
227
- <div className="flex flex-col gap-2">
228
- {desc && <Description description={desc} />}
229
- <SwapPanel
230
- amount={amount}
231
- setAmount={setAmount}
232
- srcToken={srcToken}
233
- setSrcToken={setSrcToken}
234
- srcChain={srcChain}
853
+ // XPay form
854
+ <form
855
+ className="xpay-flex xpay-flex-col xpay-gap-2 xpay-z-10 xpay-my-0 p-2 md:xpay-p-4 xpay-rounded-3xl xpay-overflow-hidden xpay-font-light sm:xpay-w-x_desktop"
856
+ onSubmit={handleSubmit}
857
+ >
858
+ <TopBar
235
859
  signer={signer}
236
- balances={balances}
237
- prices={prices}
238
- supportedChains={supportedChains}
239
- setSrcChain={setSrcChain}
240
- setExceedsExpressDeliveryLimit={setExceedsExpressDeliveryLimit}
241
- />
242
- <Summary
243
- isGettingRoute={isGettingRoute}
244
- route={route}
245
- dstToken={dstToken}
246
- dstChain={dstChain}
860
+ historyTabShown={historyTabShown}
861
+ setHistoryTabShown={setHistoryTabShown}
862
+ setSettingsShown={setSettingsShown}
863
+ onClose={onClose}
247
864
  />
248
- <FeesDetails
249
- route={route}
250
- isGettingRoute={isGettingRoute}
251
- paymentToken={paymentToken}
252
- dstToken={dstToken}
253
- expressChecked={expressChecked}
254
- exceedsExpressDeliveryLimit={exceedsExpressDeliveryLimit}
255
- slippage={slippage}
256
- feesDetailsShown={feesDetailsShown}
257
- setFeesDetailsShown={setFeesDetailsShown}
258
- />
259
- {formError.length > 0 && <ErrorField error={formError} />}
260
- {signer &&
261
- srcChain &&
262
- srcChain.chainId !== account.chainId?.toString() ? (
263
- <Button type="button" onClick={handleSwitchChain}>
264
- Switch chain
265
- </Button>
865
+ {settingsShown && (
866
+ <Settings
867
+ expressChecked={expressChecked}
868
+ setExpressChecked={setExpressChecked}
869
+ setSettingsShown={setSettingsShown}
870
+ slippage={slippage}
871
+ setSlippage={setSlippage}
872
+ />
873
+ )}
874
+ {historyTabShown ? (
875
+ <History signer={signer} supportedChains={supportedChains} />
266
876
  ) : (
267
- <Button
268
- type="submit"
269
- disabled={!signer || !route || isGettingRoute}
270
- >
271
- Submit
272
- </Button>
877
+ <div className="xpay-flex xpay-flex-col xpay-gap-2">
878
+ <SwapPanel
879
+ amount={amount}
880
+ setAmount={setAmount}
881
+ srcToken={srcToken}
882
+ setSrcToken={setSrcToken}
883
+ srcChain={srcChain}
884
+ signer={signer}
885
+ balances={balances}
886
+ prices={prices}
887
+ supportedChains={supportedChains}
888
+ setSrcChain={setSrcChain}
889
+ />
890
+ <Summary
891
+ isGettingRoute={isGettingRoute}
892
+ route={route}
893
+ dstToken={dstDisplayToken ? dstDisplayToken : dstToken}
894
+ dstChain={dstChain}
895
+ />
896
+ {desc && <Description description={desc} />}
897
+
898
+ <FeesDetails
899
+ route={route}
900
+ isGettingRoute={isGettingRoute}
901
+ paymentToken={paymentToken}
902
+ dstToken={dstToken}
903
+ slippage={slippage}
904
+ feesDetailsShown={feesDetailsShown}
905
+ setFeesDetailsShown={setFeesDetailsShown}
906
+ isExpressDelivery={isExpressDelivery}
907
+ />
908
+ {formError.length > 0 && <ErrorField error={formError} />}
909
+
910
+ <Button
911
+ type="submit"
912
+ disabled={swapButton.disabled}
913
+ onClick={swapButton.fn}
914
+ >
915
+ {swapButton.text}
916
+ </Button>
917
+ </div>
273
918
  )}
274
- </div>
919
+ </form>
275
920
  )}
276
- </form>
921
+ </>
277
922
  );
278
923
  };