@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.
- package/.github/workflows/main.yml +2 -1
- package/CHANGELOG.md +12 -0
- package/babel.config.cjs +8 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.global.js +132 -135
- package/dist/index.js +9 -9
- package/dist/index.mjs +10 -10
- package/jest.config.ts +37 -0
- package/package.json +17 -5
- package/src/components/Modal/index.tsx +1 -1
- package/src/components/Swap/Header/index.tsx +1 -1
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +12 -10
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +1 -1
- package/src/components/Swap/SwapView/SwapButton/index.tsx +23 -21
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +4 -2
- package/src/components/Swap/SwapView/SwapPanel/ChainPanel/ChainPicker/ChainItem/index.tsx +51 -59
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +47 -13
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +138 -35
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +6 -2
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +1 -1
- package/src/components/Swap/SwapView/index.tsx +2 -2
- package/src/components/Tooltip/index.tsx +1 -1
- package/src/context/HistoryProvider.tsx +2 -2
- package/src/context/SwapProvider.tsx +174 -100
- package/src/context/TransactionProvider.tsx +1 -1
- package/src/models/TokenData.ts +3 -1
- package/src/models/payloads/GetPricesPayload.ts +1 -1
- package/src/utils/validation.ts +50 -16
- package/tailwind.config.js +1 -0
- package/test/context/SwapProvider.test.tsx +851 -0
- package/test/fileMock.ts +1 -0
- package/test/fixtures/bridgeTokens.mock.ts +1318 -0
- package/test/fixtures/bridgeTokensDictionary.mock.ts +1272 -0
- package/test/fixtures/integrationConfig.mock.ts +10 -0
- package/test/fixtures/supportedChains.mock.ts +32950 -0
- package/test/{api.test.ts → services/getChains.test.ts} +6 -5
- package/test/setup.ts +13 -0
- package/test/styleMock.ts +1 -0
- package/jest.config.json +0 -8
- package/test/api.mock.ts +0 -106
- 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 {
|
|
53
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
...otherFilteredTokens
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
:
|
|
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
|
|
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-
|
|
11
|
-
<div className="flex flex-col gap-
|
|
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:
|
|
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 {
|
|
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
|
});
|