@xswap-link/sdk 0.14.0 → 0.15.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.
- package/CHANGELOG.md +21 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.global.js +92 -92
- package/dist/index.js +3208 -2862
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3083 -2740
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +6 -4
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +5 -7
- package/src/assets/icons/ChainlinkMarkIcon.tsx +4 -5
- package/src/assets/icons/CheckCircleThinIcon.tsx +22 -0
- package/src/assets/icons/ChevronDownThinIcon.tsx +39 -0
- package/src/assets/icons/ChevronDownWideThinIcon.tsx +18 -0
- package/src/assets/icons/ChevronsUpDownThinIcon.tsx +21 -0
- package/src/assets/icons/ClockIcon.tsx +3 -7
- package/src/assets/icons/CloseIcon.tsx +9 -4
- package/src/assets/icons/ErrorCircleThinIcon.tsx +22 -0
- package/src/assets/icons/LinkExternalThinIcon.tsx +19 -0
- package/src/assets/icons/LoadingRingIcon.tsx +22 -0
- package/src/assets/icons/ProgressRingThinIcon.tsx +29 -0
- package/src/assets/icons/SearchThinIcon.tsx +15 -0
- package/src/assets/icons/SignThinIcon.tsx +15 -0
- package/src/assets/icons/WalletCheckThinIcon.tsx +24 -0
- package/src/assets/icons/WarningIcon.tsx +8 -5
- package/src/assets/icons/index.ts +11 -0
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +6 -8
- package/src/components/Swap/Header/index.tsx +2 -4
- package/src/components/Swap/HistoryView/ActivityCard/index.tsx +110 -115
- package/src/components/Swap/HistoryView/index.tsx +10 -7
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/ReorderButton/ReorderButton.tsx +4 -4
- package/src/components/Swap/SettingsView/Delivery/index.tsx +19 -17
- package/src/components/Swap/SettingsView/InfiniteApproval/index.tsx +19 -17
- package/src/components/Swap/SettingsView/Slippage/index.tsx +55 -49
- package/src/components/Swap/SwapView/ConfirmationView/TokenTiles/index.tsx +69 -0
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Header/index.tsx +13 -25
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/Steps/index.tsx +65 -113
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +68 -26
- package/src/components/Swap/SwapView/ConfirmationView/TxResult/index.tsx +51 -74
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +1 -1
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +5 -5
- package/src/components/Swap/SwapView/SwapButton/index.tsx +2 -31
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/ChainPicker/index.tsx +143 -0
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx +19 -16
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +11 -4
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +185 -105
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +54 -120
- package/src/components/Swap/SwapView/SwapPanel/UsdValue/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +10 -16
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +4 -4
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +131 -139
- package/src/components/Swap/SwapView/index.tsx +11 -7
- package/src/components/Swap/WalletPickerView/index.tsx +65 -0
- package/src/components/Swap/index.tsx +39 -8
- package/src/components/ToggleButton/index.tsx +5 -7
- package/src/components/TokenLogo/index.tsx +1 -1
- package/src/components/TxConfigForm/index.tsx +2 -2
- package/src/components/TxModal/index.tsx +14 -4
- package/src/components/global.css +18 -1
- package/src/constants/index.ts +4 -0
- package/src/context/HistoryProvider.tsx +1 -1
- package/src/context/SwapProvider.tsx +193 -81
- package/src/context/TxUIWrapper.tsx +3 -3
- package/src/hooks/useEvmContractApi.ts +28 -4
- package/src/models/payloads/GetBalancesPayload.ts +1 -0
- package/src/services/api.ts +37 -0
- package/src/utils/index.ts +27 -1
- package/src/utils/tokens.ts +20 -1
- package/src/utils/validation.ts +9 -4
- package/tailwind.config.js +4 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/ArrowIcon.tsx +0 -13
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/SwapPanel/index.tsx +0 -72
- package/src/components/Swap/SwapView/ConfirmationView/TxResult/TokenItem/index.tsx +0 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
|
2
2
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
3
|
-
import { CloseIcon, CopyIcon,
|
|
3
|
+
import { CloseIcon, CopyIcon, SearchThinIcon } from "@src/assets/icons";
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
6
6
|
Spinner,
|
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
} from "@src/components";
|
|
10
10
|
import { SwapPanelType } from "@src/components/Swap/SwapView";
|
|
11
11
|
import { useSwapContext } from "@src/context";
|
|
12
|
-
import { ERC20Abi } from "@src/contracts";
|
|
13
|
-
import { useEvmContractApi } from "@src/hooks";
|
|
14
12
|
import { Chain, Ecosystem, ImportedTokenData, TokenOption } from "@src/models";
|
|
15
13
|
import {
|
|
14
|
+
isBridgeTokenConnectionSupported,
|
|
16
15
|
isChainConnectionSupported,
|
|
17
16
|
isETHAddressValid,
|
|
18
17
|
isServer,
|
|
@@ -21,13 +20,14 @@ import {
|
|
|
21
20
|
} from "@src/utils";
|
|
22
21
|
import { BigNumber, ethers } from "ethers";
|
|
23
22
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
24
|
-
import {
|
|
23
|
+
import { Virtuoso } from "react-virtuoso";
|
|
25
24
|
import { NetworkFilter } from "./NetworkFilter";
|
|
26
25
|
import { TokenItem } from "./TokenItem";
|
|
27
26
|
import { useAccount } from "wagmi";
|
|
28
27
|
import { Connection, PublicKey } from "@solana/web3.js";
|
|
29
28
|
import { METADATA_PROGRAM_ID } from "@src/constants";
|
|
30
29
|
import { useWallet } from "@src/context/WalletProvider";
|
|
30
|
+
import { getTokenMetadata } from "@src/services/api";
|
|
31
31
|
|
|
32
32
|
type Props = {
|
|
33
33
|
type: SwapPanelType;
|
|
@@ -55,7 +55,11 @@ export const TokenPicker = ({
|
|
|
55
55
|
const [loadingCustomTokenData, setLoadingCustomTokenData] = useState(false);
|
|
56
56
|
const [customTokenData, setCustomTokenData] =
|
|
57
57
|
useState<ImportedTokenData | null>(null);
|
|
58
|
-
|
|
58
|
+
// Always opens on "All Networks" — the cross-chain token list is the
|
|
59
|
+
// default view even when a chain is already set on the swap panel.
|
|
60
|
+
const [networkFilter, setNetworkFilter] = useState<Chain | undefined>(
|
|
61
|
+
undefined,
|
|
62
|
+
);
|
|
59
63
|
const currentRouteRequestNonce = useRef<number>(0);
|
|
60
64
|
const tokenSearchRef = useRef<HTMLInputElement | null>(null);
|
|
61
65
|
|
|
@@ -67,21 +71,26 @@ export const TokenPicker = ({
|
|
|
67
71
|
findTokenDataByAddressAndChain,
|
|
68
72
|
tokenPrices,
|
|
69
73
|
addTokenToChainIfNotExists,
|
|
74
|
+
bridgeTokensDictionary,
|
|
70
75
|
} = useSwapContext();
|
|
71
76
|
const { address } = useAccount();
|
|
72
77
|
const {
|
|
73
78
|
solana: { address: solanaAddress },
|
|
74
79
|
} = useWallet();
|
|
75
|
-
const evmContractApi = useEvmContractApi();
|
|
76
80
|
|
|
77
81
|
useEffect(() => {
|
|
78
82
|
setShowImportWarning(false);
|
|
79
83
|
setAcceptRisks(false);
|
|
80
84
|
}, [isOpen]);
|
|
81
85
|
|
|
86
|
+
// Custom token imports follow the network filter, not the chain currently
|
|
87
|
+
// set on the swap panel — the picker lists tokens across networks, so a
|
|
88
|
+
// pasted address is resolved on the network the user is browsing.
|
|
89
|
+
const importChain = networkFilter ?? chain;
|
|
90
|
+
|
|
82
91
|
const importCustomToken = useCallback(() => {
|
|
83
92
|
if (
|
|
84
|
-
|
|
93
|
+
importChain &&
|
|
85
94
|
customTokenData &&
|
|
86
95
|
(isETHAddressValid(searchValue) || isSolanaAddressValid(searchValue))
|
|
87
96
|
) {
|
|
@@ -101,12 +110,12 @@ export const TokenPicker = ({
|
|
|
101
110
|
quickPick: false,
|
|
102
111
|
supported: true,
|
|
103
112
|
priority: 0,
|
|
104
|
-
chainId:
|
|
113
|
+
chainId: importChain.chainId,
|
|
105
114
|
};
|
|
106
115
|
|
|
107
116
|
if (!isServer) {
|
|
108
|
-
customTokens[
|
|
109
|
-
...(customTokens[
|
|
117
|
+
customTokens[importChain.chainId] = {
|
|
118
|
+
...(customTokens[importChain.chainId] || {}),
|
|
110
119
|
[searchValue.toLowerCase()]: newCustomToken,
|
|
111
120
|
};
|
|
112
121
|
localStorage.setItem("custom-tokens", JSON.stringify(customTokens));
|
|
@@ -117,7 +126,7 @@ export const TokenPicker = ({
|
|
|
117
126
|
setModalOpen(false);
|
|
118
127
|
}
|
|
119
128
|
}, [
|
|
120
|
-
|
|
129
|
+
importChain,
|
|
121
130
|
customTokenData,
|
|
122
131
|
searchValue,
|
|
123
132
|
addTokenToChainIfNotExists,
|
|
@@ -125,42 +134,32 @@ export const TokenPicker = ({
|
|
|
125
134
|
]);
|
|
126
135
|
|
|
127
136
|
const getCustomTokenData = useCallback(async () => {
|
|
137
|
+
if (!importChain) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
128
140
|
const nonce = Date.now();
|
|
129
141
|
currentRouteRequestNonce.current = nonce;
|
|
130
|
-
const rpcUrl = supportedChains.find(
|
|
131
|
-
({ ecosystem, chainId }) =>
|
|
132
|
-
ecosystem === Ecosystem.EVM && chainId === chain?.chainId,
|
|
133
|
-
)?.publicRpcUrls[0];
|
|
134
|
-
const customTokenContract = new ethers.Contract(
|
|
135
|
-
searchValue.toLowerCase(),
|
|
136
|
-
ERC20Abi,
|
|
137
|
-
ethers.getDefaultProvider(rpcUrl),
|
|
138
|
-
);
|
|
139
142
|
setLoadingCustomTokenData(true);
|
|
140
143
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
// Resolved server-side through private RPCs — public ones often 429
|
|
145
|
+
const tokenData = await getTokenMetadata({
|
|
146
|
+
chainId: importChain.chainId,
|
|
147
|
+
tokenAddress: searchValue.toLowerCase(),
|
|
148
|
+
});
|
|
146
149
|
if (nonce !== currentRouteRequestNonce.current) {
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
setCustomTokenData({
|
|
151
|
-
address: searchValue.toLowerCase(),
|
|
152
|
-
decimals,
|
|
153
|
-
name,
|
|
154
|
-
symbol,
|
|
155
|
-
});
|
|
152
|
+
setCustomTokenData(tokenData);
|
|
156
153
|
} catch (_) {
|
|
157
|
-
|
|
154
|
+
if (nonce === currentRouteRequestNonce.current) {
|
|
155
|
+
setCustomTokenData(null);
|
|
156
|
+
}
|
|
158
157
|
} finally {
|
|
159
158
|
if (nonce === currentRouteRequestNonce.current) {
|
|
160
159
|
setLoadingCustomTokenData(false);
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
|
-
}, [
|
|
162
|
+
}, [importChain, searchValue]);
|
|
164
163
|
|
|
165
164
|
const getCustomSolanaTokenData = useCallback(async () => {
|
|
166
165
|
const nonce = Date.now();
|
|
@@ -168,7 +167,7 @@ export const TokenPicker = ({
|
|
|
168
167
|
|
|
169
168
|
const rpcUrl = supportedChains.find(
|
|
170
169
|
({ ecosystem, chainId }) =>
|
|
171
|
-
ecosystem === Ecosystem.SOLANA && chainId ===
|
|
170
|
+
ecosystem === Ecosystem.SOLANA && chainId === importChain?.chainId,
|
|
172
171
|
)?.publicRpcUrls[0];
|
|
173
172
|
|
|
174
173
|
if (!rpcUrl || !searchValue || !isSolanaAddressValid(searchValue)) {
|
|
@@ -258,23 +257,26 @@ export const TokenPicker = ({
|
|
|
258
257
|
setLoadingCustomTokenData(false);
|
|
259
258
|
}
|
|
260
259
|
}
|
|
261
|
-
}, [
|
|
260
|
+
}, [importChain, searchValue, supportedChains]);
|
|
262
261
|
|
|
263
262
|
useEffect(() => {
|
|
264
263
|
if (
|
|
265
264
|
!bridgeUI &&
|
|
266
|
-
|
|
265
|
+
importChain &&
|
|
267
266
|
!findTokenDataByAddressAndChain(
|
|
268
|
-
|
|
267
|
+
importChain.ecosystem === Ecosystem.EVM
|
|
269
268
|
? searchValue.toLowerCase()
|
|
270
269
|
: searchValue,
|
|
271
|
-
|
|
270
|
+
importChain.chainId,
|
|
272
271
|
)
|
|
273
272
|
) {
|
|
274
|
-
if (
|
|
273
|
+
if (
|
|
274
|
+
importChain.ecosystem === Ecosystem.EVM &&
|
|
275
|
+
isETHAddressValid(searchValue)
|
|
276
|
+
) {
|
|
275
277
|
getCustomTokenData();
|
|
276
278
|
} else if (
|
|
277
|
-
|
|
279
|
+
importChain.ecosystem === Ecosystem.SOLANA &&
|
|
278
280
|
isSolanaAddressValid(searchValue)
|
|
279
281
|
) {
|
|
280
282
|
getCustomSolanaTokenData();
|
|
@@ -292,13 +294,12 @@ export const TokenPicker = ({
|
|
|
292
294
|
getCustomTokenData,
|
|
293
295
|
getCustomSolanaTokenData,
|
|
294
296
|
findTokenDataByAddressAndChain,
|
|
295
|
-
|
|
297
|
+
importChain,
|
|
296
298
|
]);
|
|
297
299
|
|
|
298
300
|
useEffect(() => {
|
|
299
301
|
setSearchValue("");
|
|
300
|
-
setNetworkFilter(
|
|
301
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
302
|
+
setNetworkFilter(undefined);
|
|
302
303
|
}, [isOpen]);
|
|
303
304
|
|
|
304
305
|
const isMatch = (value, searchValue) =>
|
|
@@ -330,6 +331,47 @@ export const TokenPicker = ({
|
|
|
330
331
|
[tokenPrices],
|
|
331
332
|
);
|
|
332
333
|
|
|
334
|
+
const getResetWarning = useCallback(
|
|
335
|
+
(token: TokenOption): SwapPanelType | null => {
|
|
336
|
+
if (type === "destination") {
|
|
337
|
+
if (
|
|
338
|
+
srcChain &&
|
|
339
|
+
!isChainConnectionSupported(srcChain, token.chainId, bridgeUI)
|
|
340
|
+
) {
|
|
341
|
+
return "source";
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const tokenChain = supportedChains.find(
|
|
346
|
+
({ chainId }) => chainId === token.chainId,
|
|
347
|
+
);
|
|
348
|
+
if (
|
|
349
|
+
dstChain &&
|
|
350
|
+
tokenChain &&
|
|
351
|
+
(!isChainConnectionSupported(tokenChain, dstChain.chainId, bridgeUI) ||
|
|
352
|
+
(bridgeUI &&
|
|
353
|
+
!isBridgeTokenConnectionSupported({
|
|
354
|
+
token,
|
|
355
|
+
tokenChain,
|
|
356
|
+
dstChainId: dstChain.chainId,
|
|
357
|
+
bridgeTokensDictionary,
|
|
358
|
+
})))
|
|
359
|
+
) {
|
|
360
|
+
return "destination";
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
},
|
|
364
|
+
[type, srcChain, dstChain, bridgeUI, supportedChains, bridgeTokensDictionary],
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Tokens that reset the other side of the swap on selection go below
|
|
368
|
+
// the ones that can be picked freely.
|
|
369
|
+
const compareByResetWarning = useCallback(
|
|
370
|
+
(tokenA: TokenOption, tokenB: TokenOption) =>
|
|
371
|
+
Number(!!getResetWarning(tokenA)) - Number(!!getResetWarning(tokenB)),
|
|
372
|
+
[getResetWarning],
|
|
373
|
+
);
|
|
374
|
+
|
|
333
375
|
const ownedTokens = useMemo(() => {
|
|
334
376
|
if (!address && !solanaAddress) {
|
|
335
377
|
return [];
|
|
@@ -337,6 +379,10 @@ export const TokenPicker = ({
|
|
|
337
379
|
return visibleTokens
|
|
338
380
|
.filter((token) => BigNumber.from(token?.balance || "0").gt(0))
|
|
339
381
|
.sort((tokenA, tokenB) => {
|
|
382
|
+
const warningOrder = compareByResetWarning(tokenA, tokenB);
|
|
383
|
+
if (warningOrder !== 0) {
|
|
384
|
+
return warningOrder;
|
|
385
|
+
}
|
|
340
386
|
const valueA = calculateTokenValue(tokenA);
|
|
341
387
|
const valueB = calculateTokenValue(tokenB);
|
|
342
388
|
// First sort by USD value
|
|
@@ -346,7 +392,13 @@ export const TokenPicker = ({
|
|
|
346
392
|
// If USD values are equal (or both 0), sort by priority
|
|
347
393
|
return tokenB.priority - tokenA.priority;
|
|
348
394
|
});
|
|
349
|
-
}, [
|
|
395
|
+
}, [
|
|
396
|
+
visibleTokens,
|
|
397
|
+
address,
|
|
398
|
+
solanaAddress,
|
|
399
|
+
calculateTokenValue,
|
|
400
|
+
compareByResetWarning,
|
|
401
|
+
]);
|
|
350
402
|
|
|
351
403
|
const recommendedTokens = useMemo(
|
|
352
404
|
() =>
|
|
@@ -357,9 +409,11 @@ export const TokenPicker = ({
|
|
|
357
409
|
BigNumber.from(token?.balance || "0").eq(0),
|
|
358
410
|
)
|
|
359
411
|
.sort(
|
|
360
|
-
(tokenDataA, tokenDataB) =>
|
|
412
|
+
(tokenDataA, tokenDataB) =>
|
|
413
|
+
compareByResetWarning(tokenDataA, tokenDataB) ||
|
|
414
|
+
tokenDataB.priority - tokenDataA.priority,
|
|
361
415
|
),
|
|
362
|
-
[visibleTokens, address, solanaAddress],
|
|
416
|
+
[visibleTokens, address, solanaAddress, compareByResetWarning],
|
|
363
417
|
);
|
|
364
418
|
|
|
365
419
|
const tokenGroups = useMemo(() => {
|
|
@@ -371,36 +425,49 @@ export const TokenPicker = ({
|
|
|
371
425
|
: [{ label: "RECOMMENDED.", tokens: recommendedTokens }];
|
|
372
426
|
}, [ownedTokens, recommendedTokens]);
|
|
373
427
|
|
|
374
|
-
|
|
375
|
-
|
|
428
|
+
// The current group's label renders OUTSIDE the scroll container (pinned
|
|
429
|
+
// above it, transparent bg), so rows physically clip on the label's bottom
|
|
430
|
+
// edge — the scroller's top edge. Labels of the following groups stay
|
|
431
|
+
// in-flow and take over the pinned slot once they reach that line.
|
|
432
|
+
const listRows = useMemo(
|
|
433
|
+
() =>
|
|
434
|
+
tokenGroups.flatMap((group, groupIndex) => {
|
|
435
|
+
const rows: (
|
|
436
|
+
| { kind: "label"; label: string; groupLabel: string }
|
|
437
|
+
| { kind: "empty"; groupLabel: string }
|
|
438
|
+
| { kind: "token"; token: TokenOption; groupLabel: string }
|
|
439
|
+
)[] =
|
|
440
|
+
groupIndex === 0
|
|
441
|
+
? []
|
|
442
|
+
: [
|
|
443
|
+
{
|
|
444
|
+
kind: "label",
|
|
445
|
+
label: group.label,
|
|
446
|
+
groupLabel: group.label,
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
if (group.tokens.length) {
|
|
450
|
+
rows.push(
|
|
451
|
+
...group.tokens.map((token) => ({
|
|
452
|
+
kind: "token" as const,
|
|
453
|
+
token,
|
|
454
|
+
groupLabel: group.label,
|
|
455
|
+
})),
|
|
456
|
+
);
|
|
457
|
+
} else {
|
|
458
|
+
rows.push({ kind: "empty", groupLabel: group.label });
|
|
459
|
+
}
|
|
460
|
+
return rows;
|
|
461
|
+
}),
|
|
376
462
|
[tokenGroups],
|
|
377
463
|
);
|
|
378
464
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
) {
|
|
386
|
-
return "source";
|
|
387
|
-
}
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
const tokenChain = supportedChains.find(
|
|
391
|
-
({ chainId }) => chainId === token.chainId,
|
|
392
|
-
);
|
|
393
|
-
if (
|
|
394
|
-
dstChain &&
|
|
395
|
-
tokenChain &&
|
|
396
|
-
!isChainConnectionSupported(tokenChain, dstChain.chainId, bridgeUI)
|
|
397
|
-
) {
|
|
398
|
-
return "destination";
|
|
399
|
-
}
|
|
400
|
-
return null;
|
|
401
|
-
},
|
|
402
|
-
[type, srcChain, dstChain, bridgeUI, supportedChains],
|
|
403
|
-
);
|
|
465
|
+
// Index of the topmost (partially) visible row, reported by Virtuoso.
|
|
466
|
+
const [topRowIndex, setTopRowIndex] = useState(0);
|
|
467
|
+
const pinnedGroupLabel =
|
|
468
|
+
listRows[Math.min(topRowIndex, listRows.length - 1)]?.groupLabel ??
|
|
469
|
+
tokenGroups[0]?.label ??
|
|
470
|
+
"";
|
|
404
471
|
|
|
405
472
|
useEffect(() => {
|
|
406
473
|
if (isOpen && tokenSearchRef.current) {
|
|
@@ -520,8 +587,8 @@ export const TokenPicker = ({
|
|
|
520
587
|
|
|
521
588
|
<div className="flex items-center justify-between gap-2 h-14 pl-4 pr-1 rounded-xl bg-t_bg_tertiary bg-opacity-[0.03] border border-solid border-t_text_primary border-opacity-5 mb-6">
|
|
522
589
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
523
|
-
<div className="
|
|
524
|
-
<
|
|
590
|
+
<div className="text-t_text_primary opacity-60 flex items-center shrink-0">
|
|
591
|
+
<SearchThinIcon />
|
|
525
592
|
</div>
|
|
526
593
|
<input
|
|
527
594
|
ref={tokenSearchRef}
|
|
@@ -549,7 +616,7 @@ export const TokenPicker = ({
|
|
|
549
616
|
<div className="flex gap-3 py-3 px-4 items-center rounded-xl bg-t_bg_tertiary bg-opacity-[0.03]">
|
|
550
617
|
<TokenLogoWithChain
|
|
551
618
|
token={customTokenData}
|
|
552
|
-
chain={
|
|
619
|
+
chain={importChain}
|
|
553
620
|
tokenLogoClassName="w-9 h-9"
|
|
554
621
|
generatedLogoClassName="w-9 h-9 text-[16px]"
|
|
555
622
|
chainLogoClassName="w-5"
|
|
@@ -560,7 +627,7 @@ export const TokenPicker = ({
|
|
|
560
627
|
{customTokenData.name}
|
|
561
628
|
</div>
|
|
562
629
|
<div className="text-xs text-t_text_primary !text-opacity-50 font-medium">
|
|
563
|
-
{
|
|
630
|
+
{importChain?.displayName}
|
|
564
631
|
</div>
|
|
565
632
|
</div>
|
|
566
633
|
<div className="text-sm text-t_text_primary text-opacity-50">
|
|
@@ -578,41 +645,54 @@ export const TokenPicker = ({
|
|
|
578
645
|
</div>
|
|
579
646
|
</div>
|
|
580
647
|
) : (
|
|
581
|
-
<div className="flex flex-col h-full">
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
)
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
648
|
+
<div className="flex flex-col h-full min-h-0">
|
|
649
|
+
{/* Pinned label of the group at the top of the list; sits
|
|
650
|
+
above the scroller, so rows clip on its bottom edge. */}
|
|
651
|
+
<p className="pb-3 text-[10px] font-medium leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left shrink-0">
|
|
652
|
+
{pinnedGroupLabel}
|
|
653
|
+
</p>
|
|
654
|
+
<div className="relative flex flex-col flex-1 min-h-0">
|
|
655
|
+
<Virtuoso
|
|
656
|
+
style={{ maxHeight: "700px", height: "100%" }}
|
|
657
|
+
totalCount={listRows.length}
|
|
658
|
+
rangeChanged={({ startIndex }) => setTopRowIndex(startIndex)}
|
|
659
|
+
itemContent={(index) => {
|
|
660
|
+
const row = listRows[index];
|
|
661
|
+
if (!row) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
if (row.kind === "label") {
|
|
665
|
+
return (
|
|
666
|
+
<p className="pt-2 pb-3 text-[10px] font-medium leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
|
|
667
|
+
{row.label}
|
|
668
|
+
</p>
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (row.kind === "empty") {
|
|
672
|
+
return (
|
|
673
|
+
<div className="text-center py-1 pb-3 text-sm text-t_text_primary text-opacity-50">
|
|
674
|
+
No token found
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return (
|
|
601
679
|
<TokenItem
|
|
602
680
|
panelType={type}
|
|
603
|
-
token={token}
|
|
681
|
+
token={row.token}
|
|
604
682
|
selectedChainId={chain?.chainId}
|
|
605
683
|
selectedTokenAddress={selectedTokenAddress}
|
|
606
|
-
resetWarning={getResetWarning(token)}
|
|
684
|
+
resetWarning={getResetWarning(row.token)}
|
|
607
685
|
onClick={() => {
|
|
608
|
-
onSelect(token);
|
|
686
|
+
onSelect(row.token);
|
|
609
687
|
setModalOpen(false);
|
|
610
688
|
}}
|
|
611
689
|
/>
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
690
|
+
);
|
|
691
|
+
}}
|
|
692
|
+
/>
|
|
693
|
+
{/* Scroll fade — rows dissolve towards the widget bottom */}
|
|
694
|
+
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-t_bg_secondary/65 to-t_bg_secondary/0" />
|
|
695
|
+
</div>
|
|
616
696
|
</div>
|
|
617
697
|
)}
|
|
618
698
|
</>
|