@xswap-link/sdk 0.8.7 → 0.9.1

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 +2 -1
  2. package/CHANGELOG.md +12 -0
  3. package/babel.config.cjs +8 -0
  4. package/dist/index.d.mts +4 -2
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.global.js +132 -135
  7. package/dist/index.js +9 -9
  8. package/dist/index.mjs +10 -10
  9. package/jest.config.ts +37 -0
  10. package/package.json +17 -5
  11. package/src/components/Modal/index.tsx +1 -1
  12. package/src/components/Swap/Header/index.tsx +1 -1
  13. package/src/components/Swap/ReorderButton/ReorderButton.tsx +12 -10
  14. package/src/components/Swap/SwapView/FeesPanel/index.tsx +1 -1
  15. package/src/components/Swap/SwapView/SwapButton/index.tsx +23 -21
  16. package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +4 -2
  17. package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +51 -59
  18. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +47 -13
  19. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +138 -35
  20. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +6 -2
  21. package/src/components/Swap/SwapView/SwapPanel/index.tsx +1 -1
  22. package/src/components/Swap/SwapView/index.tsx +2 -2
  23. package/src/components/Tooltip/index.tsx +1 -1
  24. package/src/context/HistoryProvider.tsx +2 -2
  25. package/src/context/SwapProvider.tsx +174 -100
  26. package/src/context/TransactionProvider.tsx +1 -1
  27. package/src/models/TokenData.ts +3 -1
  28. package/src/models/payloads/GetPricesPayload.ts +1 -1
  29. package/src/utils/validation.ts +50 -16
  30. package/tailwind.config.js +1 -0
  31. package/test/context/SwapProvider.test.tsx +851 -0
  32. package/test/fileMock.ts +1 -0
  33. package/test/fixtures/bridgeTokens.mock.ts +1318 -0
  34. package/test/fixtures/bridgeTokensDictionary.mock.ts +1272 -0
  35. package/test/fixtures/integrationConfig.mock.ts +10 -0
  36. package/test/fixtures/supportedChains.mock.ts +32950 -0
  37. package/test/{api.test.ts → services/getChains.test.ts} +6 -5
  38. package/test/setup.ts +13 -0
  39. package/test/styleMock.ts +1 -0
  40. package/jest.config.json +0 -8
  41. package/test/api.mock.ts +0 -106
  42. package/test/setupTests.ts +0 -3
@@ -19,6 +19,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
19
  import { GroupedVirtuoso } from "react-virtuoso";
20
20
  import { QuickPickTokenItem } from "./QuickPickTokenItem";
21
21
  import { TokenItem } from "./TokenItem";
22
+ import { useAccount } from "wagmi";
22
23
 
23
24
  type Props = {
24
25
  type: SwapPanelType;
@@ -46,11 +47,17 @@ export const TokenPicker = ({
46
47
  const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
47
48
  const [customTokenData, setCustomTokenData] =
48
49
  useState<ImportedTokenData | null>(null);
50
+ const [showQuickPicks, setShowQuickPicks] = useState(true);
49
51
  const currentRouteRequestNonce = useRef<number>(0);
50
52
  const tokenSearchRef = useRef<HTMLInputElement | null>(null);
51
53
 
52
- const { bridgeUI, supportedChains, findTokenDataByAddressAndChain } =
53
- useSwapContext();
54
+ const {
55
+ bridgeUI,
56
+ supportedChains,
57
+ findTokenDataByAddressAndChain,
58
+ tokenPrices,
59
+ } = useSwapContext();
60
+ const { address } = useAccount();
54
61
  const evmContractApi = useEvmContractApi();
55
62
 
56
63
  useEffect(() => {
@@ -188,35 +195,79 @@ export const TokenPicker = ({
188
195
  }, [isOpen]);
189
196
 
190
197
  const allTokens = useMemo(() => {
191
- const filteredTokensSorted = [
192
- ...filteredTokens
193
- .filter((token) => BigNumber.from(token?.balance || "0").gt(0))
194
- .sort((tokenDataA, tokenDataB) => {
195
- return tokenDataB.priority - tokenDataA.priority;
196
- }),
197
- ...filteredTokens
198
- .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
199
- .sort((tokenDataA, tokenDataB) => {
200
- return tokenDataB.priority - tokenDataA.priority;
201
- }),
202
- ];
203
- const otherFilteredTokensSorted = [
204
- ...otherFilteredTokens
205
- .filter((token) => BigNumber.from(token?.balance || "0").gt(0))
206
- .sort((tokenDataA, tokenDataB) => {
207
- return tokenDataB.priority - tokenDataA.priority;
208
- }),
209
- ...otherFilteredTokens
210
- .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
211
- .sort((tokenDataA, tokenDataB) => {
212
- return tokenDataB.priority - tokenDataA.priority;
213
- }),
214
- ];
198
+ const calculateTokenValue = (token: TokenOption) => {
199
+ if (!token.balance || !token.decimals) return 0;
200
+ const balance = Number(
201
+ ethers.utils.formatUnits(token.balance, token.decimals),
202
+ );
203
+ const price = tokenPrices[token.chainId]?.[token.address];
204
+ return price ? balance * Number(price) : 0;
205
+ };
206
+
207
+ const ownedFilteredTokens = [
208
+ ...filteredTokens.filter((token) =>
209
+ BigNumber.from(token?.balance || "0").gt(0),
210
+ ),
211
+ ...otherFilteredTokens.filter((token) =>
212
+ BigNumber.from(token?.balance || "0").gt(0),
213
+ ),
214
+ ].sort((tokenA, tokenB) => {
215
+ const valueA = calculateTokenValue(tokenA);
216
+ const valueB = calculateTokenValue(tokenB);
217
+ // First sort by USD value
218
+ if (valueB !== valueA) {
219
+ return valueB - valueA;
220
+ }
221
+ // If USD values are equal (or both 0), sort by priority
222
+ return tokenB.priority - tokenA.priority;
223
+ });
224
+
225
+ const currentChainTokens = filteredTokens
226
+ .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
227
+ .sort(
228
+ (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
229
+ );
215
230
 
216
- return [...filteredTokensSorted, ...otherFilteredTokensSorted];
217
- }, [filteredTokens, otherFilteredTokens]);
231
+ const otherChainTokens = otherFilteredTokens
232
+ .filter((token) => BigNumber.from(token?.balance || "0").eq(0))
233
+ .sort(
234
+ (tokenDataA, tokenDataB) => tokenDataB.priority - tokenDataA.priority,
235
+ );
218
236
 
219
- const tokenGroups = [filteredTokens, otherFilteredTokens];
237
+ // Create groups based on chain and wallet connection status
238
+ const groups = chain
239
+ ? [
240
+ ...(!!address && ownedFilteredTokens.length
241
+ ? [ownedFilteredTokens]
242
+ : []),
243
+ currentChainTokens,
244
+ otherChainTokens,
245
+ ]
246
+ : [
247
+ ...(!!address && ownedFilteredTokens.length
248
+ ? [ownedFilteredTokens]
249
+ : []),
250
+ [...currentChainTokens, ...otherChainTokens],
251
+ ];
252
+
253
+ return groups.flat();
254
+ }, [filteredTokens, otherFilteredTokens, chain, address, tokenPrices]);
255
+
256
+ const ownedTokens = useMemo(
257
+ () => allTokens.filter((token) => token.balance?.gt(0)),
258
+ [allTokens],
259
+ );
260
+
261
+ const tokenGroups = chain
262
+ ? [
263
+ ...(!!address && ownedTokens.length ? [ownedTokens] : []),
264
+ filteredTokens,
265
+ otherFilteredTokens,
266
+ ]
267
+ : [
268
+ ...(!!address && ownedTokens.length ? [ownedTokens] : []),
269
+ otherFilteredTokens,
270
+ ];
220
271
 
221
272
  return showImportWarning ? (
222
273
  <>
@@ -313,6 +364,7 @@ export const TokenPicker = ({
313
364
  <CloseIcon />
314
365
  </div>
315
366
  </div>
367
+
316
368
  <TextInput
317
369
  ref={tokenSearchRef}
318
370
  inputIcon={
@@ -324,7 +376,13 @@ export const TokenPicker = ({
324
376
  value={searchValue}
325
377
  handleChange={setSearchValue}
326
378
  />
327
- <div className="flex flex-wrap gap-1 my-4 mx-0">
379
+ <div
380
+ className={`flex flex-wrap gap-1 my-4 mx-0 transition-all duration-300 ${
381
+ showQuickPicks
382
+ ? "opacity-100 max-h-[200px]"
383
+ : "opacity-0 max-h-0 overflow-hidden"
384
+ }`}
385
+ >
328
386
  {tokens
329
387
  .filter((token) => token?.quickPick)
330
388
  .map((token) => (
@@ -383,17 +441,61 @@ export const TokenPicker = ({
383
441
  <GroupedVirtuoso
384
442
  style={{ maxHeight: "400px", height: "100%" }}
385
443
  groupCounts={tokenGroups.map((group) => group?.length)}
444
+ scrollerRef={(ref) => {
445
+ if (ref) {
446
+ ref.addEventListener("scroll", (e) => {
447
+ const target = e.target as HTMLDivElement;
448
+ setShowQuickPicks(target.scrollTop < 10);
449
+ });
450
+ }
451
+ }}
386
452
  groupContent={(index) => (
387
453
  <div className="py-1 bg-t_bg_primary text-t_text_primary">
388
454
  <div className="px-4 text-left">
389
455
  {(() => {
390
456
  if (chain) {
391
- return index === 0
392
- ? `${chain.displayName} network`
393
- : "Other networks";
457
+ // chain is selected
458
+ switch (index) {
459
+ case 0: {
460
+ if (!address) {
461
+ return `${chain.displayName} network`;
462
+ }
463
+ return ownedTokens.length
464
+ ? "My tokens"
465
+ : `${chain.displayName} network`;
466
+ }
467
+ case 1: {
468
+ if (!address) {
469
+ return "Other networks";
470
+ }
471
+ return `${chain.displayName} network`;
472
+ }
473
+ case 2: {
474
+ return "Other networks";
475
+ }
476
+ default: {
477
+ return "Undefined group";
478
+ }
479
+ }
480
+ } else {
481
+ // chain is not selected
482
+ switch (index) {
483
+ case 0: {
484
+ if (!address) {
485
+ return "All networks";
486
+ }
487
+ return ownedTokens.length
488
+ ? "My tokens"
489
+ : "All networks";
490
+ }
491
+ case 1: {
492
+ return "All networks";
493
+ }
494
+ default: {
495
+ return "Undefined group";
496
+ }
497
+ }
394
498
  }
395
-
396
- return "All networks";
397
499
  })()}
398
500
  </div>
399
501
  <div className="horizontal-separator" />
@@ -407,6 +509,7 @@ export const TokenPicker = ({
407
509
  allTokens[index] && (
408
510
  <TokenItem
409
511
  token={allTokens[index]}
512
+ selectedChainId={chain?.chainId}
410
513
  selectedTokenAddress={selectedTokenAddress}
411
514
  onClick={() => {
412
515
  onSelect(allTokens[index]!);
@@ -77,7 +77,7 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
77
77
  return (
78
78
  <>
79
79
  {tokenPickerModalOpen && (
80
- <Modal onClose={() => setTokenPickerModalOpen(false)}>
80
+ <Modal onClose={() => setTokenPickerModalOpen(false)} className="z-50">
81
81
  <TokenPicker
82
82
  type={type}
83
83
  chain={chain!}
@@ -113,7 +113,11 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
113
113
  <div className="flex flex-col font-medium items-start">
114
114
  <p className="text-xs opacity-60">Token: </p>
115
115
  <p className={`${token ? "" : "opacity-60"}`}>
116
- {token ? token.symbol : "Select"}
116
+ {token
117
+ ? token.symbol
118
+ : type === "destination" && bridgeUI
119
+ ? "-"
120
+ : "Select"}
117
121
  </p>
118
122
  </div>
119
123
  </div>
@@ -38,7 +38,7 @@ export const SwapPanel = ({ type }: Props) => {
38
38
  />
39
39
  </div>
40
40
  <div
41
- className={`flex justify-between ${
41
+ className={`flex justify-between rounded-b-[11px] ${
42
42
  type === "destination" &&
43
43
  "bg-gradient-to-r from-t_main_accent_light/20 to-t_main_accent_dark/20"
44
44
  }`}
@@ -7,8 +7,8 @@ export type SwapPanelType = "source" | "destination";
7
7
 
8
8
  export const SwapView = () => {
9
9
  return (
10
- <div className="flex flex-col gap-2 fill-t_text_primary">
11
- <div className="flex flex-col gap-5">
10
+ <div className="flex flex-col gap-6 fill-t_text_primary">
11
+ <div className="flex flex-col gap-[11px]">
12
12
  <SwapPanel type="source" />
13
13
  <ReorderButton />
14
14
  <SwapPanel type="destination" />
@@ -13,7 +13,7 @@ const positionToClass: Record<Position, string> = {
13
13
 
14
14
  export const Tooltip: FC<{
15
15
  id: string;
16
- position: "TOP" | "BOTTOM" | "LEFT" | "RIGHT";
16
+ position: Position;
17
17
  text: string;
18
18
  triggerRef: RefObject<Element>;
19
19
  }> = ({ id, position, text, triggerRef }) => {
@@ -10,7 +10,7 @@ import { Transaction, TransactionStatus } from "@src/models";
10
10
  import { getHistory } from "@src/services";
11
11
  import { isServer } from "@src/utils";
12
12
  import BigNumberJS from "bignumber.js";
13
- import { closeSnackbar, useSnackbar } from "notistack";
13
+ import { useSnackbar } from "notistack";
14
14
  import {
15
15
  Context,
16
16
  ReactNode,
@@ -69,7 +69,7 @@ export const HistoryProvider = ({ children }: HistoryProviderProps) => {
69
69
  const [history, setHistory] = useState<Transaction[]>([]);
70
70
  const [historyLoadedOnce, setHistoryLoadedOnce] = useState(false);
71
71
 
72
- const { enqueueSnackbar } = useSnackbar();
72
+ const { enqueueSnackbar, closeSnackbar } = useSnackbar();
73
73
  const { debounce: getHistoryDebounce } = useDebounce({
74
74
  delayMs: 600,
75
75
  });