@xswap-link/sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.github/workflows/main.yml +1 -0
  2. package/.github/workflows/publish.yml +1 -0
  3. package/CHANGELOG.md +24 -0
  4. package/dist/index.css +128 -32
  5. package/dist/index.d.mts +22 -19
  6. package/dist/index.d.ts +22 -19
  7. package/dist/index.js +1167 -32978
  8. package/dist/index.mjs +1152 -32988
  9. package/nodemon.json +5 -0
  10. package/package.json +17 -6
  11. package/src/components/TxConfigForm/BalanceComponent.tsx +51 -0
  12. package/src/components/TxConfigForm/ChainListElement.tsx +36 -0
  13. package/src/components/TxConfigForm/Description.tsx +15 -0
  14. package/src/components/TxConfigForm/ErrorField.tsx +17 -0
  15. package/src/components/TxConfigForm/FeesDetails.tsx +105 -0
  16. package/src/components/TxConfigForm/Form.tsx +129 -447
  17. package/src/components/TxConfigForm/History.tsx +67 -122
  18. package/src/components/TxConfigForm/HistoryCard.tsx +3 -2
  19. package/src/components/TxConfigForm/Settings.tsx +137 -0
  20. package/src/components/TxConfigForm/Summary.tsx +57 -0
  21. package/src/components/TxConfigForm/SwapPanel.tsx +191 -0
  22. package/src/components/TxConfigForm/TokenPicker.tsx +76 -67
  23. package/src/components/TxConfigForm/TopBar.tsx +53 -0
  24. package/src/components/TxConfigForm/index.tsx +121 -0
  25. package/src/components/icons/ArrowDownIcon.tsx +16 -0
  26. package/src/components/icons/ChainlinkCCIPIcon.tsx +23 -0
  27. package/src/components/icons/ChevronDownIcon.tsx +2 -2
  28. package/src/components/icons/ChevronUpIcon.tsx +2 -2
  29. package/src/components/icons/CloseIcon.tsx +2 -2
  30. package/src/components/icons/CoinsIcon.tsx +8 -6
  31. package/src/components/icons/HistoryIcon.tsx +8 -5
  32. package/src/components/icons/InfoIcon.tsx +13 -0
  33. package/src/components/icons/SettingsIcon.tsx +16 -0
  34. package/src/components/icons/TimerIcon.tsx +15 -0
  35. package/src/components/icons/XSwapBadgeIcon.tsx +23 -0
  36. package/src/components/icons/index.ts +6 -0
  37. package/src/config/index.ts +1 -0
  38. package/src/config/wagmiConfig.ts +45 -0
  39. package/src/constants/index.ts +6 -3
  40. package/tailwind.config.js +11 -2
  41. package/src/components/TxConfigForm/index.jsx +0 -100
  42. package/src/components/global.d.ts +0 -6
@@ -1,129 +1,65 @@
1
1
  import { Transaction, TransactionStatus, Chain } from "@src/models";
2
2
  import React, { FC, useCallback, useEffect, useState } from "react";
3
3
  import { getHistory } from "@src/services";
4
- import { FALLBACK_CROSSCHAIN_ESTIMATION_TIME } from "@src/constants";
4
+ import { DELIVERY_TIME, EXPRESS_DELIVERY_TIME } from "@src/constants";
5
5
  import BigNumberJS from "bignumber.js";
6
6
  import { HistoryCard } from "./HistoryCard";
7
7
  import { CircularProgressIcon } from "../icons";
8
8
 
9
- const PENDING_TXS_LOCAL_STORAGE_KEY = "pending_transactions";
10
-
11
- type LocalHistory = { [walletAddress: string]: Transaction[] };
12
-
13
9
  interface Props {
14
- signer: string;
10
+ signer: string | undefined;
15
11
  supportedChains: Chain[];
16
12
  }
17
13
 
18
14
  export const History: FC<Props> = ({ signer, supportedChains }) => {
19
- const [mappedHistory, setMappedHistory] = useState<Transaction[]>();
20
- // const [latestHistory, setLatestHistory] = useState<Transaction[]>();
15
+ const [fetchedHistory, setFetchedHistory] = useState<Transaction[]>();
21
16
  const [historyLoadedOnce, setHistoryLoadedOnce] = useState(false);
22
17
 
23
- const getLocalHistory = () => {
24
- return JSON.parse(
25
- localStorage.getItem(PENDING_TXS_LOCAL_STORAGE_KEY) || "{}",
26
- ) as LocalHistory;
27
- };
28
-
29
- const setLocalHistory = (data: LocalHistory) => {
30
- localStorage.setItem(PENDING_TXS_LOCAL_STORAGE_KEY, JSON.stringify(data));
31
- };
32
-
33
18
  const fetchHistory = useCallback(async () => {
34
- const downloadedHistory = await getHistory({ walletAddress: signer });
35
- const fetchedTransactions: Transaction[] = [];
36
- if (downloadedHistory) {
37
- for (const entry of downloadedHistory.history) {
38
- if (!entry.source) {
39
- continue;
40
- }
41
- let status: TransactionStatus;
42
- if (entry.failed) status = "REVERTED";
43
- if (!!entry.target) {
44
- status = "DONE";
45
- } else {
46
- status = "IN_PROGRESS";
47
- }
48
- const timestampMs = new Date(entry.source?.blockTime).getTime();
49
-
50
- let estimatedDeliveryTimestamp = FALLBACK_CROSSCHAIN_ESTIMATION_TIME;
51
-
52
- if (new BigNumberJS(entry.source.valueForInstantCcipRecieve).gt(0)) {
53
- estimatedDeliveryTimestamp = timestampMs + 30 * 1000;
54
- } else {
55
- const blockchainEstimate = timestampMs + 30 * 1000 * 60;
56
-
57
- if (blockchainEstimate) {
58
- estimatedDeliveryTimestamp = blockchainEstimate;
59
- }
60
- }
61
-
62
- fetchedTransactions.push({
63
- hash: entry.source.transactionHash,
64
- timestamp: timestampMs / 1000,
65
- sourceChainId: entry.source.blockchainId,
66
- targetChainId: entry.source.targetBlockchainId,
67
- amountWei: entry.source.tokenAmount,
68
- tokenAddress: entry.source.tokenAddress,
69
- tokenOutAddress:
70
- entry.target?.tokenAddress || entry.source.tokenOutAddress,
71
- tokenOutAmount:
72
- entry.target?.tokenAmount || entry.source.estimatedAmountOut,
73
- estimatedDeliveryTimestamp,
74
- status,
75
- });
76
- }
77
-
78
- const txHashToTxFromAPI: Record<string, Transaction> = {};
79
- for (const transaction of fetchedTransactions) {
80
- txHashToTxFromAPI[transaction.hash] = transaction;
81
- }
82
-
83
- const localHistory = getLocalHistory();
84
- const newLocalHistory: Record<string, Transaction[]> = {};
85
-
86
- for (const [walletAddress, transactions] of Object.entries(
87
- localHistory,
88
- )) {
89
- if (walletAddress.toLowerCase() !== signer.toLowerCase()) {
90
- continue;
91
- }
92
- const newLocalHistoryForWallet: Transaction[] = [];
93
-
94
- for (const transaction of transactions) {
95
- if (!txHashToTxFromAPI[transaction.hash]) {
96
- // localStorage has fresher data.
97
- fetchedTransactions.push(transaction);
98
- newLocalHistoryForWallet.push(transaction);
19
+ if (signer) {
20
+ const downloadedHistory = await getHistory({ walletAddress: signer });
21
+ const fetchedTransactions: Transaction[] = [];
22
+ if (downloadedHistory) {
23
+ for (const entry of downloadedHistory.history) {
24
+ if (!entry.source) {
99
25
  continue;
100
26
  }
101
-
102
- if (
103
- txHashToTxFromAPI[transaction.hash] &&
104
- transaction.status === "DONE" &&
105
- txHashToTxFromAPI[transaction.hash]?.status === "IN_PROGRESS"
106
- ) {
107
- // If we know on the frontend that the tx is done we should update backend result
108
- txHashToTxFromAPI[transaction.hash]!.status = "DONE";
27
+ let status: TransactionStatus;
28
+ if (entry.failed) status = "REVERTED";
29
+ if (!!entry.target) {
30
+ status = "DONE";
31
+ } else {
32
+ status = "IN_PROGRESS";
109
33
  }
110
34
 
111
- if (
112
- txHashToTxFromAPI[transaction.hash]?.status === "DONE" ||
113
- txHashToTxFromAPI[transaction.hash]?.status === "REVERTED"
114
- ) {
115
- // API has the same or fresher data.
116
- continue;
117
- }
35
+ const blockTimestampMs = new Date(entry.source.blockTime).getTime();
36
+ const estimatedDeliveryTimestamp = new BigNumberJS(
37
+ entry.source.valueForInstantCcipRecieve,
38
+ ).gt(0)
39
+ ? blockTimestampMs + EXPRESS_DELIVERY_TIME
40
+ : blockTimestampMs + DELIVERY_TIME;
41
+
42
+ fetchedTransactions.push({
43
+ hash: entry.source.transactionHash,
44
+ timestamp: blockTimestampMs / 1000,
45
+ sourceChainId: entry.source.blockchainId,
46
+ targetChainId: entry.source.targetBlockchainId,
47
+ amountWei: entry.source.tokenAmount,
48
+ tokenAddress: entry.source.tokenAddress,
49
+ tokenOutAddress:
50
+ entry.target?.tokenAddress || entry.source.tokenOutAddress,
51
+ tokenOutAmount:
52
+ entry.target?.tokenAmount || entry.source.estimatedAmountOut,
53
+ estimatedDeliveryTimestamp,
54
+ status,
55
+ });
118
56
  }
119
57
 
120
- newLocalHistory[walletAddress.toLowerCase()] = newLocalHistoryForWallet;
58
+ setFetchedHistory(
59
+ fetchedTransactions.sort((a, b) => b.timestamp - a.timestamp),
60
+ );
61
+ setHistoryLoadedOnce(true);
121
62
  }
122
- setLocalHistory(newLocalHistory);
123
- setMappedHistory(
124
- fetchedTransactions.sort((a, b) => b.timestamp - a.timestamp),
125
- );
126
- setHistoryLoadedOnce(true);
127
63
  }
128
64
  }, [signer, getHistory]);
129
65
 
@@ -135,25 +71,34 @@ export const History: FC<Props> = ({ signer, supportedChains }) => {
135
71
  return () => clearInterval(timer);
136
72
  }, [signer, fetchHistory, getHistory, historyLoadedOnce]);
137
73
 
74
+ if (!signer)
75
+ return (
76
+ <div className="flex items-center justify-center w-full h-[30vh]">
77
+ Connect a wallet to browse history
78
+ </div>
79
+ );
80
+
138
81
  return (
139
- <div className="w-full h-[50vh] overflow-scroll overflow-x-hidden">
140
- {mappedHistory?.length === 0 && historyLoadedOnce && (
141
- <div className="w-full text-center py-4 px-0 text-[rgb(158,158,158)]">
142
- Your history is empty...
143
- </div>
144
- )}
145
- {!mappedHistory && !historyLoadedOnce && (
146
- <div className="flex w-full h-full items-center justify-center">
147
- <CircularProgressIcon />
148
- </div>
149
- )}
150
- {mappedHistory?.map((transaction, index) => (
151
- <HistoryCard
152
- key={index}
153
- transaction={transaction}
154
- supportedChains={supportedChains}
155
- />
156
- ))}
82
+ <div className="flex items-center justify-center w-full">
83
+ <div className="w-full h-[30vh] overflow-scroll overflow-x-hidden">
84
+ {fetchedHistory?.length === 0 && historyLoadedOnce && (
85
+ <div className="w-full text-center py-4 px-0 text-[rgb(158,158,158)]">
86
+ Your history is empty...
87
+ </div>
88
+ )}
89
+ {!fetchedHistory && !historyLoadedOnce && (
90
+ <div className="flex w-full h-full items-center justify-center">
91
+ <CircularProgressIcon />
92
+ </div>
93
+ )}
94
+ {fetchedHistory?.map((transaction, index) => (
95
+ <HistoryCard
96
+ key={index}
97
+ transaction={transaction}
98
+ supportedChains={supportedChains}
99
+ />
100
+ ))}
101
+ </div>
157
102
  </div>
158
103
  );
159
104
  };
@@ -9,6 +9,7 @@ import {
9
9
  HourGlassIcon,
10
10
  XMarkIcon,
11
11
  } from "@src/components/icons";
12
+ import { MINIMUM_DISPLAYED_TOKEN_AMOUNT } from "@src/constants";
12
13
 
13
14
  interface Props {
14
15
  transaction: Transaction;
@@ -158,8 +159,8 @@ export const HistoryCard: FC<Props> = ({ transaction, supportedChains }) => {
158
159
  <div className="flex items-center mb-2 last:mb-0">
159
160
  <div className="flex flex-1 items-center flex-wrap text-xs text-[rgba(255,255,255,0.6)]">
160
161
  <div className="flex items-center">
161
- {Number(transferredAmount) < 0.0001
162
- ? "<0.0001"
162
+ {Number(transferredAmount) < MINIMUM_DISPLAYED_TOKEN_AMOUNT
163
+ ? `<${MINIMUM_DISPLAYED_TOKEN_AMOUNT}`
163
164
  : transferredAmount}
164
165
  <div className="text-xs ml-1 text-rgba(255,255,255,0.6)">
165
166
  {tokenData?.symbol}
@@ -0,0 +1,137 @@
1
+ import { CloseIcon, InfoIcon, PercentageIcon } from "../icons";
2
+ import { SLIPPAGE_PRESETS } from "@src/constants";
3
+ import { FC, useState, useEffect } from "react";
4
+ import { NUMBER_INPUT_REGEX } from "@src/constants";
5
+
6
+ interface Props {
7
+ slippage: string;
8
+ setSlippage: React.Dispatch<React.SetStateAction<string>>;
9
+ setSettingsShown: React.Dispatch<React.SetStateAction<boolean>>;
10
+ expressChecked: boolean;
11
+ setExpressChecked: React.Dispatch<React.SetStateAction<boolean>>;
12
+ }
13
+
14
+ export const Settings: FC<Props> = ({
15
+ setSlippage,
16
+ setSettingsShown,
17
+ slippage,
18
+ expressChecked,
19
+ setExpressChecked,
20
+ }) => {
21
+ const [slippageActivePresetIndex, setSlippageActivePresetIndex] = useState(1);
22
+ const [usingSlippageInput, setUsingSlippageInput] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (!usingSlippageInput) {
26
+ setSlippage(
27
+ SLIPPAGE_PRESETS[slippageActivePresetIndex]?.toString() || "1.5",
28
+ );
29
+ }
30
+ }, [slippageActivePresetIndex, usingSlippageInput]);
31
+
32
+ return (
33
+ <div className="absolute w-[310px] right-0 top-[46px] z-10 bg-black border border-solid border-[rgba(54,129,198,1)] p-4 rounded-xl">
34
+ <div className="flex flex-col gap-4 justify-between">
35
+ <div className="flex justify-between">
36
+ <div className="text-base">Settings</div>
37
+ <div
38
+ className="cursor-pointer"
39
+ onClick={() => setSettingsShown(false)}
40
+ >
41
+ <CloseIcon />
42
+ </div>
43
+ </div>
44
+ <div>
45
+ <div className="flex gap-2 items-center">
46
+ <div className="text-sm text-[rgba(255,255,255,0.6)]">
47
+ Express delivery
48
+ </div>
49
+ <div>
50
+ <span
51
+ className="inline-flex w-14 h-9 p-3 relative align-middle box-border overflow-hidden cursor-pointer"
52
+ onClick={() => {
53
+ setExpressChecked((x) => !x);
54
+ }}
55
+ >
56
+ <span className="h-full, w-full rounded-lg bg-[rgba(255,255,255,0.3)]"></span>
57
+ <span
58
+ className={`transition-all w-5 h-5 rounded-full absolute translate-y-[-4px]
59
+ ${expressChecked ? "translate-x-[12px] bg-x_blue" : "bg-white"} `}
60
+ ></span>
61
+ </span>
62
+ </div>
63
+ </div>
64
+ <div className="flex gap-4">
65
+ <div className="min-w-[26px] min-h-[26px] text-[#ffa726]">
66
+ <InfoIcon />
67
+ </div>
68
+ <div className="text-xs text-left">
69
+ Express delivery is a special feature of XSwap that reduces
70
+ transaction time across chains to around 30 seconds. It is
71
+ currently available for swaps below a value of $ 1000 USD.
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <div className="flex flex-col gap-2 ">
77
+ <div className=" text-[rgba(255,255,255,0.6)] text-sm">Slippage</div>
78
+ <div className="flex gap-2">
79
+ <div className="flex items-center bg-[rgba(15,15,15,1)] p-1 globalBorder rounded-xl">
80
+ {SLIPPAGE_PRESETS.map((preset, index) => (
81
+ <div
82
+ key={index}
83
+ className={`transition-all cursor-pointer block rounded-lg p-2 border-none text-sm leading-[10px] ${
84
+ index === slippageActivePresetIndex && !usingSlippageInput
85
+ ? "text-white"
86
+ : "text-[rgba(255,255,255,0.2)]"
87
+ } ${
88
+ index === slippageActivePresetIndex && !usingSlippageInput
89
+ ? "bg-gradient-to-r from-[#3681c6] to-[#2b4a9d]"
90
+ : ""
91
+ }`}
92
+ onClick={() => {
93
+ setUsingSlippageInput(false);
94
+ setSlippageActivePresetIndex(index);
95
+ }}
96
+ >
97
+ {`${preset.toFixed(1)}%`}
98
+ </div>
99
+ ))}
100
+ </div>
101
+ <div className="bg-[rgba(39,39,39,1)] p-2 border border-solid border-[rgba(82,82,82,1)] rounded-xl text-white flex items-center">
102
+ <input
103
+ className="text-white border-none bg-[rgba(39,39,39,1)] w-10 placeholder:text-[rgba(255,255,255,0.2)] focus:outline-none text-sm leading-none"
104
+ placeholder="1.5"
105
+ value={usingSlippageInput ? slippage : ""}
106
+ onChange={(e) => {
107
+ if (e.target.value.length > 0) {
108
+ if (NUMBER_INPUT_REGEX.test(e.target.value)) {
109
+ setUsingSlippageInput(true);
110
+ setSlippage(e.target.value.replace(/,/g, "."));
111
+ }
112
+ } else {
113
+ setUsingSlippageInput(false);
114
+ setSlippageActivePresetIndex(1);
115
+ }
116
+ }}
117
+ />
118
+ <PercentageIcon />
119
+ </div>
120
+ </div>
121
+ <div className="flex gap-4">
122
+ <div className="min-w-[26px] min-h-[26px] text-[#ffa726]">
123
+ <InfoIcon />
124
+ </div>
125
+ <div className="text-xs text-left">
126
+ Slippage is the price variation you are willing to accept in the
127
+ event that the price of the trade changes while it is processing.
128
+ <br /> <br />
129
+ If the trade fails due to too-low slippage, you will receive USDC
130
+ on the destination chain.
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ );
137
+ };
@@ -0,0 +1,57 @@
1
+ import { FC } from "react";
2
+ import { weiToHumanReadable } from "@src/utils";
3
+ import { Chain, Route, Token } from "@src/models";
4
+ import { ArrowDownIcon } from "../icons";
5
+
6
+ interface Props {
7
+ isGettingRoute: boolean;
8
+ amount: string;
9
+ route: Route | undefined;
10
+ dstToken: Token | undefined;
11
+ dstChain: Chain | undefined;
12
+ }
13
+
14
+ export const Summary: FC<Props> = ({
15
+ isGettingRoute,
16
+ amount,
17
+ route,
18
+ dstToken,
19
+ dstChain,
20
+ }) => {
21
+ return (
22
+ <div className="flex justify-between globalBorder rounded-lg bg-[rgba(15,15,15,1)] p-4 relative">
23
+ <div className="flex flex-col gap-2">
24
+ <p className="text-[rgba(255,255,255,0.6)] text-sm">You receive</p>
25
+ {isGettingRoute && amount.length > 0 ? (
26
+ <div className="bg-current rounded animate-pulse w-[190px] h-7" />
27
+ ) : (
28
+ <div className="flex gap-2 items-center text-white text-2xl">
29
+ {weiToHumanReadable({
30
+ amount: route?.estAmountOut || "0",
31
+ decimals: dstToken?.decimals || 18,
32
+ precisionFractionalPlaces: 5,
33
+ })}
34
+ <div className="w-6 h-6">
35
+ <img src={dstToken?.image} alt={dstToken?.name} />
36
+ </div>
37
+ <p>{dstToken?.symbol}</p>
38
+ </div>
39
+ )}
40
+ </div>
41
+ <div className="flex flex-col gap-1 text-sm">
42
+ <p className="text-[rgba(255,255,255,0.6)] text-right">on chain:</p>
43
+ <div className="flex gap-1 text-white">
44
+ <div className="w-4 h-4">
45
+ <img src={dstChain?.image} alt={dstChain?.name} />
46
+ </div>
47
+ <div>{dstChain?.displayName}</div>
48
+ </div>
49
+ </div>
50
+ <div className="absolute right-[50%] top-0 translate-x-1/2 translate-y-[-60%] globalBorder rounded-xl p-[5px] bg-[rgba(15,15,15,1)]">
51
+ <div className=" flex items-center justify-center rounded-lg w-8 h-8 bg-gradient-to-l from-[rgba(54,129,198,1)] to-[rgba(43,74,157,1)] ">
52
+ <ArrowDownIcon />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ );
57
+ };
@@ -0,0 +1,191 @@
1
+ import { FC, useState, useEffect, useRef, useMemo } from "react";
2
+ import { NUMBER_INPUT_REGEX } from "@src/constants";
3
+ import { DownArrowIcon } from "../icons";
4
+ import { Token, Chain, TokenBalances, TokenPrices } from "@src/models";
5
+ import { TokenPicker } from "./TokenPicker";
6
+ import { weiToHumanReadable } from "@src/utils";
7
+ import { Alert } from "../Alert";
8
+ import { ChainListElement } from "./ChainListElement";
9
+ import { BalanceComponent } from "./BalanceComponent";
10
+ import BigNumber from "bignumber.js";
11
+
12
+ interface Props {
13
+ amount: string;
14
+ setAmount: React.Dispatch<React.SetStateAction<string>>;
15
+ srcToken: Token | undefined;
16
+ setSrcToken: React.Dispatch<React.SetStateAction<Token | undefined>>;
17
+ setSrcChain: React.Dispatch<React.SetStateAction<Chain | undefined>>;
18
+ srcChain: Chain | undefined;
19
+ signer: string | undefined;
20
+ balances: TokenBalances | undefined;
21
+ prices: TokenPrices | undefined;
22
+ supportedChains: Chain[];
23
+ }
24
+
25
+ export const SwapPanel: FC<Props> = ({
26
+ amount,
27
+ setAmount,
28
+ srcToken,
29
+ setSrcToken,
30
+ srcChain,
31
+ setSrcChain,
32
+ signer,
33
+ balances,
34
+ prices,
35
+ supportedChains,
36
+ }) => {
37
+ const [chainListShown, setChainListShown] = useState(false);
38
+ const [tokenListShown, setTokenListShown] = useState(false);
39
+ const listRef = useRef(null);
40
+ const buttonRef = useRef(null);
41
+
42
+ const handleClickOutside = (event: MouseEvent) => {
43
+ if (listRef.current && buttonRef.current) {
44
+ const listElement = listRef.current as HTMLElement;
45
+ const buttonElement = buttonRef.current as HTMLElement;
46
+ if (
47
+ !listElement.contains(event.target as Node) &&
48
+ !buttonElement.contains(event.target as Node)
49
+ )
50
+ setChainListShown(false);
51
+ }
52
+ };
53
+
54
+ const handleMaxClick = () => {
55
+ if (
56
+ balances &&
57
+ srcToken &&
58
+ balances[srcToken.address] &&
59
+ balances[srcToken.address]?.toString() !== "0"
60
+ )
61
+ setAmount(
62
+ weiToHumanReadable({
63
+ amount: balances[srcToken.address]?.toString() || "0",
64
+ decimals: srcToken.decimals,
65
+ precisionFractionalPlaces: 4,
66
+ }),
67
+ );
68
+ };
69
+
70
+ useEffect(() => {
71
+ if (chainListShown) {
72
+ document.addEventListener("mousedown", handleClickOutside);
73
+ return () =>
74
+ document.removeEventListener("mousedown", handleClickOutside);
75
+ }
76
+ }, [chainListShown]);
77
+
78
+ const chainListOptions = useMemo(
79
+ () =>
80
+ supportedChains.filter((chain) => chain.chainId !== srcChain?.chainId),
81
+ [supportedChains, srcChain],
82
+ );
83
+
84
+ return (
85
+ <div className="flex flex-col items-start globalBorder rounded-lg bg-[rgba(15,15,15,1)] p-4">
86
+ <div className="w-full flex gap-4 items-start justify-between mb-4">
87
+ <div className="flex flex-col gap-2 overflow-hidden w-1/2">
88
+ <p className="text-white opacity-60 text-sm font-medium">You pay</p>
89
+ <input
90
+ className="p-0 border-none outline-none bg-transparent text-2xl overflow-ellipsis"
91
+ value={amount}
92
+ onChange={(e) => {
93
+ if (e.target.value === ".") return;
94
+ if (NUMBER_INPUT_REGEX.test(e.target.value)) {
95
+ setAmount(e.target.value.replace(/,/g, "."));
96
+ }
97
+ }}
98
+ inputMode="decimal"
99
+ spellCheck="false"
100
+ autoComplete="off"
101
+ autoCorrect="off"
102
+ placeholder="0"
103
+ />
104
+ </div>
105
+ <div className="flex flex-col relative items-end gap-2.5 w-1/2 text-xs">
106
+ <div
107
+ ref={buttonRef}
108
+ className="bg-black globalBorder rounded-2xl whitespace-nowrap flex items-center p-2 cursor-pointer gap-2"
109
+ onClick={() => setChainListShown((prev) => !prev)}
110
+ >
111
+ <img
112
+ width="16"
113
+ height="16"
114
+ src={srcChain?.image}
115
+ alt={srcChain?.name}
116
+ />
117
+ <div>{srcChain?.displayName}</div>
118
+ <DownArrowIcon />
119
+ </div>
120
+
121
+ {chainListShown && (
122
+ <ul
123
+ ref={listRef}
124
+ className="bg-black globalBorder rounded-lg whitespace-nowrap z-1 right-0 top-10 px-2 max-w-full max-h-[40vh] overflow-auto absolute text-sm z-20"
125
+ >
126
+ {chainListOptions.map((chain, index) => (
127
+ <ChainListElement
128
+ index={index}
129
+ length={supportedChains.length}
130
+ setSrcChain={setSrcChain}
131
+ setChainListShown={setChainListShown}
132
+ chain={chain}
133
+ />
134
+ ))}
135
+ </ul>
136
+ )}
137
+ <div
138
+ className="flex gap-2 items-center p-2 max-w-full cursor-pointer bg-black text-white globalBorder rounded-3xl whitespace-nowrap"
139
+ onClick={() => {
140
+ setTokenListShown((state) => !state);
141
+ }}
142
+ >
143
+ <img
144
+ height={16}
145
+ width={16}
146
+ src={srcToken?.image}
147
+ alt={srcToken?.name}
148
+ />
149
+ <div>{srcToken?.symbol}</div>
150
+ <DownArrowIcon />
151
+ </div>
152
+
153
+ {tokenListShown && (
154
+ <TokenPicker
155
+ onCloseClick={() => setTokenListShown(false)}
156
+ tokens={srcChain?.tokens}
157
+ signer={signer}
158
+ selectedToken={srcToken}
159
+ setSelectedToken={(token) => setSrcToken(token)}
160
+ balances={balances}
161
+ />
162
+ )}
163
+ </div>
164
+ </div>
165
+
166
+ <div className="flex justify-between items-center w-full text-sm text-[rgba(255,255,255,0.6)] relative">
167
+ {srcToken ? (
168
+ prices && prices[srcToken.address] ? (
169
+ <div>
170
+ {`$${(Number(amount) * Number(prices[srcToken.address])).toFixed(
171
+ 2,
172
+ )}`}
173
+ </div>
174
+ ) : (
175
+ <Alert desc="Unknown price" />
176
+ )
177
+ ) : (
178
+ <>$0.00</>
179
+ )}
180
+ <div
181
+ onClick={handleMaxClick}
182
+ className="flex gap-1 items-center font-medium text-white opacity-60 py-0.5 cursor-pointer"
183
+ >
184
+ {signer && (
185
+ <BalanceComponent balances={balances} srcToken={srcToken} />
186
+ )}
187
+ </div>
188
+ </div>
189
+ </div>
190
+ );
191
+ };