@xswap-link/sdk 0.3.5 → 0.4.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 (199) hide show
  1. package/.eslintrc.json +9 -5
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +2 -1
  4. package/dist/index.d.mts +106 -20
  5. package/dist/index.d.ts +106 -20
  6. package/dist/index.global.js +963 -164
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +105 -3564
  9. package/dist/index.js.map +1 -0
  10. package/dist/index.mjs +105 -3534
  11. package/dist/index.mjs.map +1 -0
  12. package/nodemon.json +1 -1
  13. package/package.json +16 -9
  14. package/postcss.config.js +8 -1
  15. package/src/{components → assets}/icons/ArrowDownIcon.tsx +1 -2
  16. package/src/assets/icons/ArrowDownLongIcon.tsx +12 -0
  17. package/src/{components → assets}/icons/ArrowLeftIcon.tsx +1 -5
  18. package/src/{components → assets}/icons/ArrowRightIcon.tsx +1 -5
  19. package/src/assets/icons/ArrowUpIcon.tsx +12 -0
  20. package/src/{components → assets}/icons/ArrowUpRightIcon.tsx +1 -5
  21. package/src/assets/icons/CashbacklIcon.tsx +28 -0
  22. package/src/{components → assets}/icons/CheckIcon.tsx +1 -5
  23. package/src/assets/icons/ChevronDownIcon.tsx +12 -0
  24. package/src/assets/icons/ChevronUpIcon.tsx +7 -0
  25. package/src/{components → assets}/icons/CircularProgressIcon.tsx +1 -2
  26. package/src/assets/icons/CloseIcon.tsx +15 -0
  27. package/src/{components → assets}/icons/CoinsIcon.tsx +0 -2
  28. package/src/assets/icons/CopyIcon.tsx +15 -0
  29. package/src/assets/icons/DotGreenIcon.tsx +12 -0
  30. package/src/{components → assets}/icons/DownArrorIcon.tsx +0 -2
  31. package/src/{components → assets}/icons/ErrorIcon.tsx +0 -1
  32. package/src/assets/icons/ErrorModalStatusIcon.tsx +31 -0
  33. package/src/assets/icons/HelpIcon.tsx +21 -0
  34. package/src/assets/icons/HistoryGradientBgIcon.tsx +32 -0
  35. package/src/assets/icons/HistoryIcon.tsx +12 -0
  36. package/src/{components → assets}/icons/HourGlassIcon.tsx +1 -5
  37. package/src/assets/icons/Icon.tsx +5 -0
  38. package/src/{components → assets}/icons/InfoIcon.tsx +0 -1
  39. package/src/assets/icons/LinkExternalIcon.tsx +15 -0
  40. package/src/{components → assets}/icons/PercentageIcon.tsx +0 -1
  41. package/src/assets/icons/RankIcon.tsx +28 -0
  42. package/src/assets/icons/SettingsGradientBgIcon.tsx +32 -0
  43. package/src/assets/icons/SettingsIcon.tsx +12 -0
  44. package/src/{components → assets}/icons/SignIcon.tsx +0 -2
  45. package/src/{components → assets}/icons/SuccessIcon.tsx +0 -1
  46. package/src/assets/icons/SuccessModalStatusIcon.tsx +31 -0
  47. package/src/assets/icons/ThumbUpRoundedIcon.tsx +15 -0
  48. package/src/{components → assets}/icons/TimerIcon.tsx +0 -1
  49. package/src/assets/icons/TokensIcon.tsx +16 -0
  50. package/src/assets/icons/WaitingIcon.tsx +15 -0
  51. package/src/{components → assets}/icons/XMarkIcon.tsx +1 -5
  52. package/src/assets/icons/XPowerIcon.tsx +28 -0
  53. package/src/{components → assets}/icons/index.ts +16 -0
  54. package/src/components/Alert/index.tsx +1 -1
  55. package/src/components/Button/index.tsx +57 -0
  56. package/src/components/MessageBar/SnackMessage.tsx +32 -0
  57. package/src/components/MessageBar/index.tsx +44 -0
  58. package/src/components/Modal/index.tsx +52 -0
  59. package/src/components/PoweredBy/index.tsx +12 -0
  60. package/src/components/SafeInput/index.tsx +51 -0
  61. package/src/components/Skeleton/index.tsx +5 -9
  62. package/src/components/Spinner/index.tsx +6 -8
  63. package/src/components/Swap/Header/Controls/index.tsx +57 -0
  64. package/src/components/Swap/Header/index.tsx +16 -0
  65. package/src/components/Swap/HistoryView/index.tsx +34 -0
  66. package/src/components/Swap/SettingsView/Delivery/index.tsx +42 -0
  67. package/src/components/Swap/SettingsView/Slippage/index.tsx +80 -0
  68. package/src/components/Swap/SettingsView/index.tsx +11 -0
  69. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/ArrowIcon.tsx +13 -0
  70. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +37 -0
  71. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Steps/index.tsx +145 -0
  72. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/SwapPanel/index.tsx +61 -0
  73. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +291 -0
  74. package/src/components/Swap/SwapView/ConfirmationView/TxResult/TokenItem/index.tsx +24 -0
  75. package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +84 -0
  76. package/src/components/Swap/SwapView/ConfirmationView/index.tsx +17 -0
  77. package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +25 -0
  78. package/src/components/Swap/SwapView/FeesPanel/Fee/index.tsx +18 -0
  79. package/src/components/Swap/SwapView/FeesPanel/Fees/index.tsx +71 -0
  80. package/src/components/Swap/SwapView/FeesPanel/index.tsx +67 -0
  81. package/src/components/Swap/SwapView/SwapButton/index.tsx +154 -0
  82. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +23 -0
  83. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +56 -0
  84. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +70 -0
  85. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/index.tsx +62 -0
  86. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/index.tsx +74 -0
  87. package/src/components/Swap/SwapView/SwapPanel/MaxPanel/index.tsx +35 -0
  88. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/QuickPickTokenItem/index.tsx +40 -0
  89. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +62 -0
  90. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +399 -0
  91. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +104 -0
  92. package/src/components/Swap/SwapView/SwapPanel/index.tsx +42 -0
  93. package/src/components/Swap/SwapView/index.tsx +20 -0
  94. package/src/components/Swap/index.tsx +34 -0
  95. package/src/components/TextInput/index.tsx +67 -0
  96. package/src/components/Timer/index.tsx +57 -0
  97. package/src/components/ToggleButton/index.tsx +24 -0
  98. package/src/components/{UnknownTokenLogo/UnknownTokenLogo.tsx → TokenLogo/Unknown/index.tsx} +3 -4
  99. package/src/components/TokenLogo/WithChain/index.tsx +34 -0
  100. package/src/components/TokenLogo/index.tsx +22 -0
  101. package/src/components/Tooltip/index.tsx +108 -0
  102. package/src/components/TxConfigForm/index.tsx +63 -42
  103. package/src/components/TxDataCard/TokenDetails/index.tsx +30 -0
  104. package/src/components/TxDataCard/TxDataCardUI/index.tsx +133 -0
  105. package/src/components/TxDataCard/index.tsx +30 -0
  106. package/src/components/TxStatusButton/index.tsx +64 -108
  107. package/src/components/global.css +115 -4
  108. package/src/components/index.ts +18 -1
  109. package/src/config/fonts.ts +114 -0
  110. package/src/config/global.d.ts +4 -0
  111. package/src/config/init.tsx +35 -31
  112. package/src/constants/crosschainEstimationTimes.ts +52 -0
  113. package/src/constants/index.ts +8 -0
  114. package/src/context/GlobalDataProvider.tsx +165 -0
  115. package/src/context/HistoryProvider.tsx +339 -0
  116. package/src/context/ModalProvider.tsx +47 -0
  117. package/src/context/SwapProvider.tsx +664 -0
  118. package/src/context/TransactionProvider.tsx +86 -0
  119. package/src/context/TxUIWrapper.tsx +388 -0
  120. package/src/context/index.ts +5 -0
  121. package/src/contracts/abi/CustomXSwapRouter.json +953 -0
  122. package/src/contracts/abi/StakingStableAPR.json +1438 -0
  123. package/src/contracts/abi/XSwapRouter.json +1242 -0
  124. package/src/contracts/abi/XSwapTokenFeeDistributor.json +350 -0
  125. package/src/contracts/abi/index.ts +8 -0
  126. package/src/contracts/addresses.ts +24 -6
  127. package/src/fonts/Satoshi-Black.woff2 +0 -0
  128. package/src/fonts/Satoshi-BlackItalic.woff2 +0 -0
  129. package/src/fonts/Satoshi-Bold.woff2 +0 -0
  130. package/src/fonts/Satoshi-BoldItalic.woff2 +0 -0
  131. package/src/fonts/Satoshi-Light.woff2 +0 -0
  132. package/src/fonts/Satoshi-LightItalic.woff2 +0 -0
  133. package/src/fonts/Satoshi-Medium.woff2 +0 -0
  134. package/src/fonts/Satoshi-MediumItalic.woff2 +0 -0
  135. package/src/fonts/Satoshi-Regular.woff2 +0 -0
  136. package/src/fonts/Satoshi-RegularItalic.woff2 +0 -0
  137. package/src/fonts/Satoshi-Variable.woff2 +0 -0
  138. package/src/fonts/Satoshi-VariableItalic.woff2 +0 -0
  139. package/src/hooks/index.ts +5 -0
  140. package/src/hooks/networkManagement/clientToSigner.tsx +17 -0
  141. package/src/hooks/networkManagement/useNetworks.ts +51 -0
  142. package/src/hooks/useBatchQuery.ts +46 -0
  143. package/src/hooks/useDebounce.tsx +28 -0
  144. package/src/hooks/useEvmContractApi.ts +85 -0
  145. package/src/hooks/usePortal.ts +79 -0
  146. package/src/hooks/useXPower.ts +58 -0
  147. package/src/models/Addresses.ts +8 -2
  148. package/src/models/TokenData.ts +13 -0
  149. package/src/models/TransactionHistory.ts +2 -0
  150. package/src/models/TxUIWrapper.ts +51 -0
  151. package/src/models/XSwapConfig.ts +1 -0
  152. package/src/models/index.ts +1 -0
  153. package/src/models/payloads/GetBalancesPayload.ts +3 -0
  154. package/src/models/payloads/GetFinishedLeaderboardQuestsPayload.ts +3 -0
  155. package/src/models/payloads/GetLeaderboardChangePayload.ts +4 -0
  156. package/src/models/payloads/{GetSwapTxPayload.ts → ModalIntegrationPayload.ts} +3 -1
  157. package/src/models/payloads/index.ts +5 -2
  158. package/src/services/api.ts +34 -5
  159. package/src/services/integrations/monitoring.ts +49 -37
  160. package/src/services/integrations/transactions.ts +50 -6
  161. package/src/utils/contracts.ts +29 -0
  162. package/src/utils/index.ts +3 -3
  163. package/src/utils/numbers.ts +14 -0
  164. package/src/utils/parseWeb3Error.ts +7 -3
  165. package/tailwind.config.js +15 -5
  166. package/tsconfig.json +7 -7
  167. package/tsup.config.ts +61 -0
  168. package/xswap.config.ts +1 -0
  169. package/dist/index.css +0 -1
  170. package/src/components/TxConfigForm/BalanceComponent.tsx +0 -52
  171. package/src/components/TxConfigForm/Button.tsx +0 -20
  172. package/src/components/TxConfigForm/ChainListElement.tsx +0 -36
  173. package/src/components/TxConfigForm/ConfirmationAmount.tsx +0 -91
  174. package/src/components/TxConfigForm/ConfirmationView/ErrorView.tsx +0 -38
  175. package/src/components/TxConfigForm/ConfirmationView/SuccessView.tsx +0 -159
  176. package/src/components/TxConfigForm/ConfirmationView/index.tsx +0 -149
  177. package/src/components/TxConfigForm/ConnectWalletPrompt.tsx +0 -21
  178. package/src/components/TxConfigForm/Description.tsx +0 -17
  179. package/src/components/TxConfigForm/ErrorField.tsx +0 -19
  180. package/src/components/TxConfigForm/FeesDetails.tsx +0 -117
  181. package/src/components/TxConfigForm/Form.tsx +0 -574
  182. package/src/components/TxConfigForm/History.tsx +0 -131
  183. package/src/components/TxConfigForm/HistoryCard.tsx +0 -182
  184. package/src/components/TxConfigForm/PoweredBy.tsx +0 -12
  185. package/src/components/TxConfigForm/Settings.tsx +0 -143
  186. package/src/components/TxConfigForm/Summary.tsx +0 -105
  187. package/src/components/TxConfigForm/SwapPanel.tsx +0 -185
  188. package/src/components/TxConfigForm/TokenPicker.tsx +0 -176
  189. package/src/components/TxConfigForm/TopBar.tsx +0 -58
  190. package/src/components/TxConfigForm/UsdPrice.tsx +0 -44
  191. package/src/components/icons/ChevronDownIcon.tsx +0 -15
  192. package/src/components/icons/ChevronUpIcon.tsx +0 -10
  193. package/src/components/icons/CloseIcon.tsx +0 -15
  194. package/src/components/icons/HistoryIcon.tsx +0 -16
  195. package/src/components/icons/SettingsIcon.tsx +0 -16
  196. /package/src/{components → assets}/icons/ChainlinkCCIPIcon.tsx +0 -0
  197. /package/src/{components → assets}/icons/SearchIcon.tsx +0 -0
  198. /package/src/{components → assets}/icons/XSwapBadgeIcon.tsx +0 -0
  199. /package/src/{components → assets}/icons/XSwapLogo.tsx +0 -0
@@ -0,0 +1,74 @@
1
+ import { ArrowDownIcon } from "@src/assets/icons";
2
+ import { useSwapContext } from "@src/context";
3
+ import { useModal } from "@src/context/ModalProvider";
4
+ import { Chain } from "@src/models";
5
+ import { useEffect, useMemo } from "react";
6
+ import { useAccount } from "wagmi";
7
+ import { SwapPanelType } from "../../../SwapView";
8
+ import { ChainPicker } from "./ChainPicker";
9
+
10
+ type Props = {
11
+ chain: Chain | undefined;
12
+ type: SwapPanelType;
13
+ };
14
+
15
+ export const ChainPanel = ({ chain, type }: Props) => {
16
+ const { supportedChains, srcChain, setSrcChain, setDstChain } =
17
+ useSwapContext();
18
+
19
+ const { chainId } = useAccount();
20
+
21
+ // select chain from user wallet
22
+ useEffect(() => {
23
+ if (!chainId) return;
24
+
25
+ const userChainFromWallet = supportedChains.find(
26
+ (chain) => chain.chainId === String(chainId),
27
+ );
28
+ if (
29
+ !userChainFromWallet ||
30
+ srcChain?.chainId === userChainFromWallet.chainId
31
+ )
32
+ return;
33
+
34
+ setSrcChain(userChainFromWallet);
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ }, [chainId]);
37
+
38
+ const pickerDisabled = useMemo(() => type === "destination", [type]);
39
+
40
+ const { openModal } = useModal();
41
+
42
+ return (
43
+ <>
44
+ <button
45
+ type="button"
46
+ onClick={() =>
47
+ openModal(
48
+ <ChainPicker
49
+ type={type}
50
+ supportedChains={supportedChains}
51
+ setSrcChain={setSrcChain}
52
+ setDstChain={setDstChain}
53
+ />,
54
+ )
55
+ }
56
+ disabled={pickerDisabled}
57
+ >
58
+ <div className="flex justify-between items-center p-4 gap-8">
59
+ <div className="flex flex-col font-medium items-start">
60
+ <p className="text-xs opacity-60">
61
+ {type === "source" ? "From:" : "To:"}
62
+ </p>
63
+ <p className={`${chain ? "" : "opacity-60"}`}>
64
+ {chain ? chain.displayName : "Select"}
65
+ </p>
66
+ </div>
67
+ <div className={`w-4 h-2 fill-white ${pickerDisabled && "hidden"}`}>
68
+ <ArrowDownIcon />
69
+ </div>
70
+ </div>
71
+ </button>
72
+ </>
73
+ );
74
+ };
@@ -0,0 +1,35 @@
1
+ import { useSwapContext } from "@src/context";
2
+ import { weiToHumanReadable } from "@src/utils";
3
+ import { SwapPanelType } from "../../index";
4
+
5
+ type Props = {
6
+ type: SwapPanelType;
7
+ };
8
+ export const MaxPanel = ({ type }: Props) => {
9
+ const { srcToken, srcTokenBalanceWei, srcTokenBalanceInfo, setSrcValue } =
10
+ useSwapContext();
11
+
12
+ return (
13
+ <button
14
+ type="button"
15
+ className={`flex items-center w-16 justify-center p-4 gap-8 ${
16
+ !srcTokenBalanceInfo && "opacity-60"
17
+ } ${
18
+ type === "source" &&
19
+ "border-l border-solid border-white border-opacity-10"
20
+ }`}
21
+ disabled={!srcTokenBalanceInfo}
22
+ onClick={() =>
23
+ setSrcValue(
24
+ weiToHumanReadable({
25
+ amount: srcTokenBalanceWei,
26
+ decimals: srcToken?.decimals || 18,
27
+ precisionFractionalPlaces: srcToken?.decimals || 18,
28
+ }),
29
+ )
30
+ }
31
+ >
32
+ {type === "source" && <p className={`border-b border-solid`}>Max</p>}
33
+ </button>
34
+ );
35
+ };
@@ -0,0 +1,40 @@
1
+ import { TokenLogo } from "@src/components";
2
+ import { useSwapContext } from "@src/context";
3
+ import { TokenOption } from "@src/models";
4
+ import { useMemo } from "react";
5
+
6
+ type Props = {
7
+ token: TokenOption;
8
+ selectedTokenAddress: string | undefined;
9
+ onClick: () => void;
10
+ };
11
+ export const QuickPickTokenItem = ({
12
+ token,
13
+ selectedTokenAddress,
14
+ onClick,
15
+ }: Props) => {
16
+ const { supportedChains } = useSwapContext();
17
+
18
+ const chain = useMemo(
19
+ () => supportedChains.find(({ chainId }) => chainId === token.chainId),
20
+ [supportedChains, token],
21
+ );
22
+
23
+ return (
24
+ <button
25
+ type="button"
26
+ className={`flex gap-2 py-2 pr-[14px] pl-2 items-center border border-solid border-white border-opacity-10 rounded-3xl text-start hover:bg-white hover:bg-opacity-10 ${
27
+ token.address === selectedTokenAddress
28
+ ? "bg-white !bg-opacity-10"
29
+ : "bg-x_gray_500"
30
+ }`}
31
+ onClick={onClick}
32
+ >
33
+ <TokenLogo token={token} className="w-6" generatedLogoClassName="w-6" />
34
+ <div className="flex flex-col gap-1 text-xs font-medium !leading-3">
35
+ <div>{token.symbol}</div>
36
+ <div className="opacity-60">{chain?.displayName}</div>
37
+ </div>
38
+ </button>
39
+ );
40
+ };
@@ -0,0 +1,62 @@
1
+ import { Skeleton, TokenLogoWithChain } from "@src/components";
2
+ import { useSwapContext } from "@src/context";
3
+ import { TokenOption } from "@src/models";
4
+ import { weiToHumanReadable } from "@src/utils";
5
+ import { ethers } from "ethers";
6
+ import { useMemo } from "react";
7
+
8
+ type Props = {
9
+ token: TokenOption;
10
+ selectedTokenAddress: string | undefined;
11
+ onClick: () => void;
12
+ };
13
+ export const TokenItem = ({ token, selectedTokenAddress, onClick }: Props) => {
14
+ const { supportedChains } = useSwapContext();
15
+
16
+ const chain = useMemo(
17
+ () => supportedChains.find(({ chainId }) => chainId === token.chainId),
18
+ [supportedChains, token],
19
+ );
20
+
21
+ return (
22
+ <div
23
+ className={`token-picker-container ${
24
+ token.address === selectedTokenAddress && "bg-white bg-opacity-10"
25
+ }`}
26
+ onClick={onClick}
27
+ >
28
+ <TokenLogoWithChain
29
+ token={token}
30
+ chain={chain}
31
+ tokenLogoClassName="w-9"
32
+ generatedLogoClassName="w-9 min-w-9 h-9 text-[16px]"
33
+ chainLogoClassName="w-5"
34
+ />
35
+ <div className="flex justify-between items-center gap-1 w-full">
36
+ <div>
37
+ <div className="text-sm font-medium text-ellipsis">{token.name}</div>
38
+ <div className="text-xs text-white !text-opacity-60 font-medium">
39
+ {chain?.displayName}
40
+ </div>
41
+ </div>
42
+ {token.balance && token.decimals ? (
43
+ <div className="text-xs">
44
+ {token.balance.eq(0) ||
45
+ Number(ethers.utils.formatUnits(token.balance, token.decimals)) >
46
+ 0.0001
47
+ ? weiToHumanReadable({
48
+ amount: token.balance.toString(),
49
+ decimals: token.decimals,
50
+ precisionFractionalPlaces: 4,
51
+ })
52
+ : "<0.0001"}
53
+ </div>
54
+ ) : (
55
+ <div>
56
+ <Skeleton className="w-[48px] h-[12px]" />
57
+ </div>
58
+ )}
59
+ </div>
60
+ </div>
61
+ );
62
+ };
@@ -0,0 +1,399 @@
1
+ import { faCheck } from "@fortawesome/free-solid-svg-icons";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { CloseIcon, CopyIcon, SearchIcon } from "@src/assets/icons";
4
+ import {
5
+ Button,
6
+ Spinner,
7
+ TextInput,
8
+ TokenLogoWithChain,
9
+ UnknownTokenLogo,
10
+ } from "@src/components";
11
+ import { SwapPanelType } from "@src/components/Swap/SwapView";
12
+ import { useSwapContext } from "@src/context";
13
+ import { useModal } from "@src/context/ModalProvider";
14
+ import { ERC20Abi } from "@src/contracts";
15
+ import { useEvmContractApi } from "@src/hooks";
16
+ import { Chain, Ecosystem, ImportedTokenData, TokenOption } from "@src/models";
17
+ import { isETHAddressValid, shortAddress } from "@src/utils";
18
+ import { BigNumber, ethers } from "ethers";
19
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
20
+ import { GroupedVirtuoso } from "react-virtuoso";
21
+ import { QuickPickTokenItem } from "./QuickPickTokenItem";
22
+ import { TokenItem } from "./TokenItem";
23
+
24
+ type Props = {
25
+ type: SwapPanelType;
26
+ chain: Chain;
27
+ tokens: TokenOption[];
28
+ otherTokens: TokenOption[];
29
+ selectedTokenAddress?: string;
30
+ isOpen: boolean;
31
+ onSelect: (token: TokenOption) => void;
32
+ setModalOpen: (isModalOpen: boolean) => void;
33
+ };
34
+ export const TokenPicker = ({
35
+ chain,
36
+ tokens,
37
+ otherTokens,
38
+ selectedTokenAddress,
39
+ isOpen,
40
+ onSelect,
41
+ setModalOpen,
42
+ type,
43
+ }: Props) => {
44
+ const [searchValue, setSearchValue] = useState("");
45
+ const [acceptRisks, setAcceptRisks] = useState(false);
46
+ const [showImportWarning, setShowImportWarning] = useState(false);
47
+ const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
48
+ const [customTokenData, setCustomTokenData] =
49
+ useState<ImportedTokenData | null>(null);
50
+ const currentRouteRequestNonce = useRef<number>(0);
51
+ const tokenSearchRef = useRef<HTMLInputElement | null>(null);
52
+
53
+ const { supportedChains, findTokenDataByAddressAndChain } = useSwapContext();
54
+ const evmContractApi = useEvmContractApi();
55
+ const { closeModal } = useModal();
56
+
57
+ useEffect(() => {
58
+ setShowImportWarning(false);
59
+ setAcceptRisks(false);
60
+ }, [isOpen]);
61
+
62
+ const importCustomToken = useCallback(() => {
63
+ if (customTokenData && isETHAddressValid(searchValue)) {
64
+ const customTokens = JSON.parse(
65
+ localStorage.getItem("custom-tokens") || "{}",
66
+ );
67
+ const newCustomToken = {
68
+ address: customTokenData.address,
69
+ symbol: customTokenData.symbol,
70
+ name: customTokenData.name,
71
+ decimals: customTokenData.decimals,
72
+ quickPick: false,
73
+ supported: true,
74
+ priority: 0,
75
+ chainId: chain.chainId,
76
+ };
77
+ customTokens[chain.chainId] = {
78
+ ...(customTokens[chain.chainId] || {}),
79
+ [searchValue.toLowerCase()]: newCustomToken,
80
+ };
81
+ localStorage.setItem("custom-tokens", JSON.stringify(customTokens));
82
+ // recalculateSupportedTokens(); TODO?
83
+ onSelect(newCustomToken);
84
+ setModalOpen(false);
85
+ }
86
+ }, [chain, searchValue, customTokenData, onSelect, setModalOpen]);
87
+
88
+ const getCustomTokenData = useCallback(async () => {
89
+ const nonce = Date.now();
90
+ currentRouteRequestNonce.current = nonce;
91
+ const rpcUrl = supportedChains.find(
92
+ ({ ecosystem, chainId }) =>
93
+ ecosystem === Ecosystem.EVM && chainId === chain.chainId,
94
+ )?.publicRpcUrls[0];
95
+ const customTokenContract = new ethers.Contract(
96
+ searchValue.toLowerCase(),
97
+ ERC20Abi,
98
+ ethers.getDefaultProvider(rpcUrl),
99
+ );
100
+ setLoadingCustomTokenData(true);
101
+ try {
102
+ const responses = await Promise.all([
103
+ evmContractApi.query(customTokenContract, "decimals"),
104
+ evmContractApi.query(customTokenContract, "name"),
105
+ evmContractApi.query(customTokenContract, "symbol"),
106
+ ]);
107
+ if (nonce !== currentRouteRequestNonce.current) {
108
+ return;
109
+ }
110
+ const [decimals, name, symbol] = responses;
111
+ setCustomTokenData({
112
+ address: searchValue.toLowerCase(),
113
+ decimals,
114
+ name,
115
+ symbol,
116
+ });
117
+ } catch (_) {
118
+ setCustomTokenData(null);
119
+ } finally {
120
+ if (nonce === currentRouteRequestNonce.current) {
121
+ setLoadingCustomTokenData(false);
122
+ }
123
+ }
124
+ }, [chain, searchValue, evmContractApi, supportedChains]);
125
+
126
+ useEffect(() => {
127
+ if (
128
+ isETHAddressValid(searchValue) &&
129
+ !findTokenDataByAddressAndChain(searchValue.toLowerCase(), chain.chainId)
130
+ ) {
131
+ getCustomTokenData();
132
+ } else {
133
+ setLoadingCustomTokenData(false);
134
+ setCustomTokenData(null);
135
+ }
136
+ }, [searchValue, getCustomTokenData, findTokenDataByAddressAndChain, chain]);
137
+
138
+ useEffect(() => {
139
+ setSearchValue("");
140
+ }, [isOpen]);
141
+
142
+ const isMatch = (value, searchValue) =>
143
+ value.toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
144
+
145
+ const filteredTokens = useMemo(
146
+ () =>
147
+ tokens?.filter(
148
+ (token) =>
149
+ isMatch(token.symbol, searchValue) ||
150
+ isMatch(token.name, searchValue) ||
151
+ token.address === searchValue.toLowerCase(),
152
+ ),
153
+ [searchValue, tokens],
154
+ );
155
+
156
+ const otherFilteredTokens = useMemo(
157
+ () =>
158
+ otherTokens?.filter(
159
+ (token) =>
160
+ isMatch(token.symbol, searchValue) ||
161
+ isMatch(token.name, searchValue) ||
162
+ token.address === searchValue.toLowerCase(),
163
+ ),
164
+ [searchValue, otherTokens],
165
+ );
166
+
167
+ useEffect(() => {
168
+ if (isOpen && tokenSearchRef.current) {
169
+ setTimeout(() => {
170
+ tokenSearchRef.current?.focus();
171
+ }, 10); // delay focus a tiny bit to ensure it works
172
+ }
173
+ }, [isOpen]);
174
+
175
+ const allTokens = useMemo(() => {
176
+ const filteredTokensSorted = [
177
+ ...filteredTokens
178
+ .filter((token) => BigNumber.from(token?.balance || "0").gt(0))
179
+ .sort((tokenDataA, tokenDataB) => {
180
+ return tokenDataB.priority - tokenDataA.priority;
181
+ }),
182
+ ...filteredTokens
183
+ .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
184
+ .sort((tokenDataA, tokenDataB) => {
185
+ return tokenDataB.priority - tokenDataA.priority;
186
+ }),
187
+ ];
188
+ const otherFilteredTokensSorted = [
189
+ ...otherFilteredTokens
190
+ .filter((token) => BigNumber.from(token?.balance || "0").gt(0))
191
+ .sort((tokenDataA, tokenDataB) => {
192
+ return tokenDataB.priority - tokenDataA.priority;
193
+ }),
194
+ ...otherFilteredTokens
195
+ .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
196
+ .sort((tokenDataA, tokenDataB) => {
197
+ return tokenDataB.priority - tokenDataA.priority;
198
+ }),
199
+ ];
200
+
201
+ return [...filteredTokensSorted, ...otherFilteredTokensSorted];
202
+ }, [filteredTokens, otherFilteredTokens]);
203
+
204
+ const tokenGroups = [filteredTokens, otherFilteredTokens];
205
+
206
+ return showImportWarning ? (
207
+ <>
208
+ <div className="flex justify-between">
209
+ <div className="text-base/4 mb-4">Import token</div>
210
+ <div className="w-4 h-4 fill-white cursor-pointer" onClick={closeModal}>
211
+ <CloseIcon />
212
+ </div>
213
+ </div>
214
+ <div className="flex flex-col h-full gap-2">
215
+ {customTokenData && (
216
+ <div
217
+ className={`token-picker-container grow-0 bg-x_gray_500 border border-solid border-white border-opacity-10 rounded-xl py-2 pr-2 pl-4 hover:cursor-pointer`}
218
+ >
219
+ <UnknownTokenLogo
220
+ tokenName={customTokenData.name}
221
+ className="w-9 h-9 text-[15px]"
222
+ />
223
+ <div className="flex justify-between items-center gap-1 overflow-hidden grow">
224
+ <div className="overflow-hidden whitespace-nowrap text-ellipsis text-sm font-medium">
225
+ {customTokenData.name}
226
+ </div>
227
+ <div className="token-picker__all__symbol">
228
+ {customTokenData.symbol}
229
+ </div>
230
+ </div>
231
+ <div className="text-sm flex items-center">
232
+ {shortAddress(searchValue.toLowerCase())}
233
+ <button
234
+ type="button"
235
+ onClick={() =>
236
+ navigator.clipboard.writeText(
237
+ customTokenData.address.toLowerCase(),
238
+ )
239
+ }
240
+ aria-label={`Copy ${customTokenData.symbol} token address`}
241
+ className="flex items-center p-2 fill-white cursor-pointer hover:fill-x_blue_300"
242
+ >
243
+ <CopyIcon className="w-5 h-5" />
244
+ </button>
245
+ </div>
246
+ </div>
247
+ )}
248
+ <div className="px-2 py-4 rounded-xl border border-x_orange text-sm">
249
+ Ensure that you have verified the token address and confirmed its
250
+ correctness
251
+ </div>
252
+ <div className=" bg-x_gray_500 border border-solid border-white border-opacity-10 rounded-xl p-2">
253
+ <div className="text-sm mb-2">
254
+ Be mindful that anyone can create tokens, even counterfeit versions
255
+ of existing ones. Certain tokens and their technical specifications
256
+ might not be compatible with XSwap services. By importing such
257
+ custom tokens, you acknowledge and consent to the associated risks.
258
+ </div>
259
+ <button
260
+ type="button"
261
+ onClick={() => setAcceptRisks((state) => !state)}
262
+ className="flex items-center"
263
+ >
264
+ <div className="flex justify-center items-center p-1 w-6 h-6 border-2 border-white rounded-md mr-2">
265
+ {acceptRisks && (
266
+ <FontAwesomeIcon icon={faCheck} className="w-4 h-4" />
267
+ )}
268
+ </div>
269
+
270
+ <div className="font-bold">Accept</div>
271
+ </button>
272
+ </div>
273
+ <div className="flex grow items-end justify-center">
274
+ <Button
275
+ type="button"
276
+ disabled={!acceptRisks}
277
+ className={`token-picker__import-button w-full ${
278
+ !acceptRisks ? "!cursor-not-allowed" : ""
279
+ }`}
280
+ onClick={importCustomToken}
281
+ >
282
+ Import
283
+ </Button>
284
+ </div>
285
+ </div>
286
+ </>
287
+ ) : (
288
+ <>
289
+ <div className="flex justify-between">
290
+ <div className="text-base/4 mb-4">{`Pick ${type} token`}</div>
291
+ <div className="w-4 h-4 fill-white cursor-pointer" onClick={closeModal}>
292
+ <CloseIcon />
293
+ </div>
294
+ </div>
295
+ <TextInput
296
+ ref={tokenSearchRef}
297
+ inputIcon={
298
+ <div style={{ fill: "#ffffffc0" }}>
299
+ <SearchIcon />
300
+ </div>
301
+ }
302
+ placeholder="Search name or paste address"
303
+ value={searchValue}
304
+ handleChange={setSearchValue}
305
+ />
306
+ <div className="flex flex-wrap gap-1 my-4 mx-0">
307
+ {tokens
308
+ .filter((token) => token?.quickPick)
309
+ .map((token) => (
310
+ <QuickPickTokenItem
311
+ key={token.address}
312
+ token={token}
313
+ selectedTokenAddress={selectedTokenAddress}
314
+ onClick={() => {
315
+ onSelect(token);
316
+ setModalOpen(false);
317
+ }}
318
+ />
319
+ ))}
320
+ </div>
321
+ <div className="flex flex-col mx-0 h-full">
322
+ {loadingCustomTokenData ? (
323
+ <div className="flex justify-center w-full py-4 px-0">
324
+ <Spinner className="w-9 h-9" />
325
+ </div>
326
+ ) : (
327
+ <>
328
+ {customTokenData ? (
329
+ <div className={`token-picker-container hover:cursor-default`}>
330
+ <TokenLogoWithChain
331
+ token={customTokenData}
332
+ chain={chain}
333
+ tokenLogoClassName="w-9"
334
+ chainLogoClassName="w-5"
335
+ />
336
+ <div className="flex justify-between items-center gap-1 overflow-hidden grow">
337
+ <div>
338
+ <div className="text-sm font-medium text-ellipsis">
339
+ {customTokenData.name}
340
+ </div>
341
+ <div className="text-xs text-white !text-opacity-60 font-medium">
342
+ {chain?.displayName}
343
+ </div>
344
+ </div>
345
+ <div className="token-picker__all__symbol">
346
+ {customTokenData.symbol}
347
+ </div>
348
+ </div>
349
+ <div>
350
+ <Button
351
+ type="button"
352
+ className="h-8 py-0 px-4 text-white leading-[14px] rounded-[32px] bg-gradient-to-r from-x_blue_300 to-x_blue_400"
353
+ onClick={() => setShowImportWarning(true)}
354
+ >
355
+ Import
356
+ </Button>
357
+ </div>
358
+ </div>
359
+ ) : (
360
+ <div className="flex flex-col h-full gap-8 w-[calc(100%+32px)] mx-[-16px]">
361
+ <GroupedVirtuoso
362
+ style={{ height: "400px" }}
363
+ groupCounts={tokenGroups.map((group) => group?.length)}
364
+ groupContent={(index) => (
365
+ <div className="py-1 bg-black">
366
+ <div className="px-4">
367
+ {index === 0
368
+ ? `${chain.displayName} network`
369
+ : "Other networks"}
370
+ </div>
371
+ <div className="horizontal-separator" />
372
+ {!tokenGroups[index]?.length && (
373
+ <div className="text-center py-1">No token found</div>
374
+ )}
375
+ </div>
376
+ )}
377
+ itemContent={(index) => {
378
+ return (
379
+ allTokens[index] && (
380
+ <TokenItem
381
+ token={allTokens[index]}
382
+ selectedTokenAddress={selectedTokenAddress}
383
+ onClick={() => {
384
+ onSelect(allTokens[index]!);
385
+ setModalOpen(false);
386
+ }}
387
+ />
388
+ )
389
+ );
390
+ }}
391
+ />
392
+ </div>
393
+ )}
394
+ </>
395
+ )}
396
+ </div>
397
+ </>
398
+ );
399
+ };