@xswap-link/sdk 0.14.0 → 0.14.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/CHANGELOG.md +15 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.global.js +86 -86
- package/dist/index.js +2129 -1857
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2225 -1953
- package/dist/index.mjs.map +1 -1
- package/localTheme.ts +5 -3
- package/package.json +1 -1
- package/src/assets/icons/AdjustmentsIcon.tsx +5 -7
- package/src/assets/icons/ClockIcon.tsx +3 -7
- package/src/assets/icons/CloseIcon.tsx +6 -2
- package/src/assets/icons/WarningIcon.tsx +8 -5
- package/src/components/Modal/index.tsx +14 -9
- package/src/components/PoweredBy/index.tsx +6 -8
- package/src/components/Swap/PickerView/index.tsx +151 -0
- package/src/components/Swap/SettingsView/Delivery/index.tsx +1 -1
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +50 -4
- package/src/components/Swap/SwapView/FeesPanel/DeliveryInfo/index.tsx +1 -1
- package/src/components/Swap/SwapView/FeesPanel/index.tsx +1 -1
- package/src/components/Swap/SwapView/SwapButton/index.tsx +2 -2
- package/src/components/Swap/SwapView/SwapPanel/AmountPanel/Balance/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 +8 -6
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +11 -4
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +102 -73
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/index.tsx +53 -119
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +1 -1
- package/src/components/Swap/index.tsx +18 -4
- package/src/components/TxConfigForm/index.tsx +2 -2
- package/src/context/HistoryProvider.tsx +1 -1
- package/src/context/SwapProvider.tsx +87 -62
- 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/tailwind.config.js +4 -1
package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/NetworkFilter/index.tsx
CHANGED
|
@@ -56,7 +56,7 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
|
|
|
56
56
|
return () => document.removeEventListener("mousedown", onPointerDown);
|
|
57
57
|
}, []);
|
|
58
58
|
|
|
59
|
-
const previewChains = chains.slice(0,
|
|
59
|
+
const previewChains = chains.slice(0, 3);
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
62
|
<div className="relative" ref={containerRef}>
|
|
@@ -72,19 +72,21 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
|
|
|
72
72
|
className="w-5 h-5 rounded-full"
|
|
73
73
|
/>
|
|
74
74
|
) : (
|
|
75
|
-
<div className="
|
|
75
|
+
<div className="flex -space-x-1.5 shrink-0">
|
|
76
76
|
{previewChains.map((chain) => (
|
|
77
77
|
<img
|
|
78
78
|
key={chain.chainId}
|
|
79
79
|
src={chain.image}
|
|
80
80
|
alt={chain.displayName}
|
|
81
|
-
className="w-
|
|
81
|
+
className="w-5 h-5 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
|
|
82
82
|
/>
|
|
83
83
|
))}
|
|
84
84
|
</div>
|
|
85
85
|
)}
|
|
86
86
|
<p className="text-sm leading-6 tracking-[0.01em] text-t_text_primary whitespace-nowrap max-w-[120px] truncate">
|
|
87
|
-
{selectedChain
|
|
87
|
+
{selectedChain
|
|
88
|
+
? selectedChain.displayName
|
|
89
|
+
: `${chains.length} Networks`}
|
|
88
90
|
</p>
|
|
89
91
|
<div
|
|
90
92
|
className={`flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 transition-transform [&_svg]:w-3 [&_svg]:h-3 ${
|
|
@@ -132,13 +134,13 @@ export const NetworkFilter = ({ type, selectedChain, onSelect }: Props) => {
|
|
|
132
134
|
className="w-5 h-5 rounded-full"
|
|
133
135
|
/>
|
|
134
136
|
) : (
|
|
135
|
-
<div className="
|
|
137
|
+
<div className="flex -space-x-1 shrink-0">
|
|
136
138
|
{previewChains.map((previewChain) => (
|
|
137
139
|
<img
|
|
138
140
|
key={previewChain.chainId}
|
|
139
141
|
src={previewChain.image}
|
|
140
142
|
alt={previewChain.displayName}
|
|
141
|
-
className="w-
|
|
143
|
+
className="w-4 h-4 rounded-full border border-solid border-t_bg_primary bg-t_bg_primary"
|
|
142
144
|
/>
|
|
143
145
|
))}
|
|
144
146
|
</div>
|
|
@@ -69,7 +69,7 @@ export const TokenItem = ({
|
|
|
69
69
|
return (
|
|
70
70
|
<div className="pb-2">
|
|
71
71
|
<div
|
|
72
|
-
className={`flex gap-3 items-center p-3 sm:p-4 rounded-xl cursor-pointer bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/[0.02] bg-
|
|
72
|
+
className={`flex gap-3 items-center p-3 sm:p-4 rounded-xl cursor-pointer bg-gradient-to-r from-t_bg_tertiary/[0.02] to-t_bg_tertiary/[0.02] bg-[rgba(40,40,40,0.15)] hover:bg-t_bg_tertiary hover:bg-opacity-10 transition-colors ${
|
|
73
73
|
isSelected ? "!bg-t_bg_tertiary !bg-opacity-10" : ""
|
|
74
74
|
}`}
|
|
75
75
|
onClick={onClick}
|
|
@@ -142,9 +142,16 @@ export const TokenItem = ({
|
|
|
142
142
|
<div className="flex items-center justify-end gap-1.5 text-[#D77F02]">
|
|
143
143
|
<WarningIcon className="w-3.5 h-3.5 shrink-0" />
|
|
144
144
|
<span className="text-xs leading-[18px] tracking-[0.01em] whitespace-nowrap">
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
<span className="sm:hidden">
|
|
146
|
+
{resetWarning === "source"
|
|
147
|
+
? "Source will reset."
|
|
148
|
+
: "Destination will reset."}
|
|
149
|
+
</span>
|
|
150
|
+
<span className="hidden sm:inline">
|
|
151
|
+
{resetWarning === "source"
|
|
152
|
+
? "Source will reset after selection."
|
|
153
|
+
: "Destination will reset after selection."}
|
|
154
|
+
</span>
|
|
148
155
|
</span>
|
|
149
156
|
</div>
|
|
150
157
|
) : (
|
|
@@ -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,
|
|
@@ -28,6 +27,7 @@ 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;
|
|
@@ -67,21 +67,26 @@ export const TokenPicker = ({
|
|
|
67
67
|
findTokenDataByAddressAndChain,
|
|
68
68
|
tokenPrices,
|
|
69
69
|
addTokenToChainIfNotExists,
|
|
70
|
+
bridgeTokensDictionary,
|
|
70
71
|
} = useSwapContext();
|
|
71
72
|
const { address } = useAccount();
|
|
72
73
|
const {
|
|
73
74
|
solana: { address: solanaAddress },
|
|
74
75
|
} = useWallet();
|
|
75
|
-
const evmContractApi = useEvmContractApi();
|
|
76
76
|
|
|
77
77
|
useEffect(() => {
|
|
78
78
|
setShowImportWarning(false);
|
|
79
79
|
setAcceptRisks(false);
|
|
80
80
|
}, [isOpen]);
|
|
81
81
|
|
|
82
|
+
// Custom token imports follow the network filter, not the chain currently
|
|
83
|
+
// set on the swap panel — the picker lists tokens across networks, so a
|
|
84
|
+
// pasted address is resolved on the network the user is browsing.
|
|
85
|
+
const importChain = networkFilter ?? chain;
|
|
86
|
+
|
|
82
87
|
const importCustomToken = useCallback(() => {
|
|
83
88
|
if (
|
|
84
|
-
|
|
89
|
+
importChain &&
|
|
85
90
|
customTokenData &&
|
|
86
91
|
(isETHAddressValid(searchValue) || isSolanaAddressValid(searchValue))
|
|
87
92
|
) {
|
|
@@ -101,12 +106,12 @@ export const TokenPicker = ({
|
|
|
101
106
|
quickPick: false,
|
|
102
107
|
supported: true,
|
|
103
108
|
priority: 0,
|
|
104
|
-
chainId:
|
|
109
|
+
chainId: importChain.chainId,
|
|
105
110
|
};
|
|
106
111
|
|
|
107
112
|
if (!isServer) {
|
|
108
|
-
customTokens[
|
|
109
|
-
...(customTokens[
|
|
113
|
+
customTokens[importChain.chainId] = {
|
|
114
|
+
...(customTokens[importChain.chainId] || {}),
|
|
110
115
|
[searchValue.toLowerCase()]: newCustomToken,
|
|
111
116
|
};
|
|
112
117
|
localStorage.setItem("custom-tokens", JSON.stringify(customTokens));
|
|
@@ -117,7 +122,7 @@ export const TokenPicker = ({
|
|
|
117
122
|
setModalOpen(false);
|
|
118
123
|
}
|
|
119
124
|
}, [
|
|
120
|
-
|
|
125
|
+
importChain,
|
|
121
126
|
customTokenData,
|
|
122
127
|
searchValue,
|
|
123
128
|
addTokenToChainIfNotExists,
|
|
@@ -125,42 +130,32 @@ export const TokenPicker = ({
|
|
|
125
130
|
]);
|
|
126
131
|
|
|
127
132
|
const getCustomTokenData = useCallback(async () => {
|
|
133
|
+
if (!importChain) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
128
136
|
const nonce = Date.now();
|
|
129
137
|
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
138
|
setLoadingCustomTokenData(true);
|
|
140
139
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
// Resolved server-side through private RPCs — public ones often 429
|
|
141
|
+
const tokenData = await getTokenMetadata({
|
|
142
|
+
chainId: importChain.chainId,
|
|
143
|
+
tokenAddress: searchValue.toLowerCase(),
|
|
144
|
+
});
|
|
146
145
|
if (nonce !== currentRouteRequestNonce.current) {
|
|
147
146
|
return;
|
|
148
147
|
}
|
|
149
|
-
|
|
150
|
-
setCustomTokenData({
|
|
151
|
-
address: searchValue.toLowerCase(),
|
|
152
|
-
decimals,
|
|
153
|
-
name,
|
|
154
|
-
symbol,
|
|
155
|
-
});
|
|
148
|
+
setCustomTokenData(tokenData);
|
|
156
149
|
} catch (_) {
|
|
157
|
-
|
|
150
|
+
if (nonce === currentRouteRequestNonce.current) {
|
|
151
|
+
setCustomTokenData(null);
|
|
152
|
+
}
|
|
158
153
|
} finally {
|
|
159
154
|
if (nonce === currentRouteRequestNonce.current) {
|
|
160
155
|
setLoadingCustomTokenData(false);
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
|
-
}, [
|
|
158
|
+
}, [importChain, searchValue]);
|
|
164
159
|
|
|
165
160
|
const getCustomSolanaTokenData = useCallback(async () => {
|
|
166
161
|
const nonce = Date.now();
|
|
@@ -168,7 +163,7 @@ export const TokenPicker = ({
|
|
|
168
163
|
|
|
169
164
|
const rpcUrl = supportedChains.find(
|
|
170
165
|
({ ecosystem, chainId }) =>
|
|
171
|
-
ecosystem === Ecosystem.SOLANA && chainId ===
|
|
166
|
+
ecosystem === Ecosystem.SOLANA && chainId === importChain?.chainId,
|
|
172
167
|
)?.publicRpcUrls[0];
|
|
173
168
|
|
|
174
169
|
if (!rpcUrl || !searchValue || !isSolanaAddressValid(searchValue)) {
|
|
@@ -258,23 +253,26 @@ export const TokenPicker = ({
|
|
|
258
253
|
setLoadingCustomTokenData(false);
|
|
259
254
|
}
|
|
260
255
|
}
|
|
261
|
-
}, [
|
|
256
|
+
}, [importChain, searchValue, supportedChains]);
|
|
262
257
|
|
|
263
258
|
useEffect(() => {
|
|
264
259
|
if (
|
|
265
260
|
!bridgeUI &&
|
|
266
|
-
|
|
261
|
+
importChain &&
|
|
267
262
|
!findTokenDataByAddressAndChain(
|
|
268
|
-
|
|
263
|
+
importChain.ecosystem === Ecosystem.EVM
|
|
269
264
|
? searchValue.toLowerCase()
|
|
270
265
|
: searchValue,
|
|
271
|
-
|
|
266
|
+
importChain.chainId,
|
|
272
267
|
)
|
|
273
268
|
) {
|
|
274
|
-
if (
|
|
269
|
+
if (
|
|
270
|
+
importChain.ecosystem === Ecosystem.EVM &&
|
|
271
|
+
isETHAddressValid(searchValue)
|
|
272
|
+
) {
|
|
275
273
|
getCustomTokenData();
|
|
276
274
|
} else if (
|
|
277
|
-
|
|
275
|
+
importChain.ecosystem === Ecosystem.SOLANA &&
|
|
278
276
|
isSolanaAddressValid(searchValue)
|
|
279
277
|
) {
|
|
280
278
|
getCustomSolanaTokenData();
|
|
@@ -292,7 +290,7 @@ export const TokenPicker = ({
|
|
|
292
290
|
getCustomTokenData,
|
|
293
291
|
getCustomSolanaTokenData,
|
|
294
292
|
findTokenDataByAddressAndChain,
|
|
295
|
-
|
|
293
|
+
importChain,
|
|
296
294
|
]);
|
|
297
295
|
|
|
298
296
|
useEffect(() => {
|
|
@@ -330,6 +328,47 @@ export const TokenPicker = ({
|
|
|
330
328
|
[tokenPrices],
|
|
331
329
|
);
|
|
332
330
|
|
|
331
|
+
const getResetWarning = useCallback(
|
|
332
|
+
(token: TokenOption): SwapPanelType | null => {
|
|
333
|
+
if (type === "destination") {
|
|
334
|
+
if (
|
|
335
|
+
srcChain &&
|
|
336
|
+
!isChainConnectionSupported(srcChain, token.chainId, bridgeUI)
|
|
337
|
+
) {
|
|
338
|
+
return "source";
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const tokenChain = supportedChains.find(
|
|
343
|
+
({ chainId }) => chainId === token.chainId,
|
|
344
|
+
);
|
|
345
|
+
if (
|
|
346
|
+
dstChain &&
|
|
347
|
+
tokenChain &&
|
|
348
|
+
(!isChainConnectionSupported(tokenChain, dstChain.chainId, bridgeUI) ||
|
|
349
|
+
(bridgeUI &&
|
|
350
|
+
!isBridgeTokenConnectionSupported({
|
|
351
|
+
token,
|
|
352
|
+
tokenChain,
|
|
353
|
+
dstChainId: dstChain.chainId,
|
|
354
|
+
bridgeTokensDictionary,
|
|
355
|
+
})))
|
|
356
|
+
) {
|
|
357
|
+
return "destination";
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
},
|
|
361
|
+
[type, srcChain, dstChain, bridgeUI, supportedChains, bridgeTokensDictionary],
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// Tokens that reset the other side of the swap on selection go below
|
|
365
|
+
// the ones that can be picked freely.
|
|
366
|
+
const compareByResetWarning = useCallback(
|
|
367
|
+
(tokenA: TokenOption, tokenB: TokenOption) =>
|
|
368
|
+
Number(!!getResetWarning(tokenA)) - Number(!!getResetWarning(tokenB)),
|
|
369
|
+
[getResetWarning],
|
|
370
|
+
);
|
|
371
|
+
|
|
333
372
|
const ownedTokens = useMemo(() => {
|
|
334
373
|
if (!address && !solanaAddress) {
|
|
335
374
|
return [];
|
|
@@ -337,6 +376,10 @@ export const TokenPicker = ({
|
|
|
337
376
|
return visibleTokens
|
|
338
377
|
.filter((token) => BigNumber.from(token?.balance || "0").gt(0))
|
|
339
378
|
.sort((tokenA, tokenB) => {
|
|
379
|
+
const warningOrder = compareByResetWarning(tokenA, tokenB);
|
|
380
|
+
if (warningOrder !== 0) {
|
|
381
|
+
return warningOrder;
|
|
382
|
+
}
|
|
340
383
|
const valueA = calculateTokenValue(tokenA);
|
|
341
384
|
const valueB = calculateTokenValue(tokenB);
|
|
342
385
|
// First sort by USD value
|
|
@@ -346,7 +389,13 @@ export const TokenPicker = ({
|
|
|
346
389
|
// If USD values are equal (or both 0), sort by priority
|
|
347
390
|
return tokenB.priority - tokenA.priority;
|
|
348
391
|
});
|
|
349
|
-
}, [
|
|
392
|
+
}, [
|
|
393
|
+
visibleTokens,
|
|
394
|
+
address,
|
|
395
|
+
solanaAddress,
|
|
396
|
+
calculateTokenValue,
|
|
397
|
+
compareByResetWarning,
|
|
398
|
+
]);
|
|
350
399
|
|
|
351
400
|
const recommendedTokens = useMemo(
|
|
352
401
|
() =>
|
|
@@ -357,9 +406,11 @@ export const TokenPicker = ({
|
|
|
357
406
|
BigNumber.from(token?.balance || "0").eq(0),
|
|
358
407
|
)
|
|
359
408
|
.sort(
|
|
360
|
-
(tokenDataA, tokenDataB) =>
|
|
409
|
+
(tokenDataA, tokenDataB) =>
|
|
410
|
+
compareByResetWarning(tokenDataA, tokenDataB) ||
|
|
411
|
+
tokenDataB.priority - tokenDataA.priority,
|
|
361
412
|
),
|
|
362
|
-
[visibleTokens, address, solanaAddress],
|
|
413
|
+
[visibleTokens, address, solanaAddress, compareByResetWarning],
|
|
363
414
|
);
|
|
364
415
|
|
|
365
416
|
const tokenGroups = useMemo(() => {
|
|
@@ -376,32 +427,6 @@ export const TokenPicker = ({
|
|
|
376
427
|
[tokenGroups],
|
|
377
428
|
);
|
|
378
429
|
|
|
379
|
-
const getResetWarning = useCallback(
|
|
380
|
-
(token: TokenOption): SwapPanelType | null => {
|
|
381
|
-
if (type === "destination") {
|
|
382
|
-
if (
|
|
383
|
-
srcChain &&
|
|
384
|
-
!isChainConnectionSupported(srcChain, token.chainId, bridgeUI)
|
|
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
|
-
);
|
|
404
|
-
|
|
405
430
|
useEffect(() => {
|
|
406
431
|
if (isOpen && tokenSearchRef.current) {
|
|
407
432
|
setTimeout(() => {
|
|
@@ -549,7 +574,7 @@ export const TokenPicker = ({
|
|
|
549
574
|
<div className="flex gap-3 py-3 px-4 items-center rounded-xl bg-t_bg_tertiary bg-opacity-[0.03]">
|
|
550
575
|
<TokenLogoWithChain
|
|
551
576
|
token={customTokenData}
|
|
552
|
-
chain={
|
|
577
|
+
chain={importChain}
|
|
553
578
|
tokenLogoClassName="w-9 h-9"
|
|
554
579
|
generatedLogoClassName="w-9 h-9 text-[16px]"
|
|
555
580
|
chainLogoClassName="w-5"
|
|
@@ -560,7 +585,7 @@ export const TokenPicker = ({
|
|
|
560
585
|
{customTokenData.name}
|
|
561
586
|
</div>
|
|
562
587
|
<div className="text-xs text-t_text_primary !text-opacity-50 font-medium">
|
|
563
|
-
{
|
|
588
|
+
{importChain?.displayName}
|
|
564
589
|
</div>
|
|
565
590
|
</div>
|
|
566
591
|
<div className="text-sm text-t_text_primary text-opacity-50">
|
|
@@ -583,12 +608,16 @@ export const TokenPicker = ({
|
|
|
583
608
|
style={{ maxHeight: "700px", height: "100%" }}
|
|
584
609
|
groupCounts={tokenGroups.map((group) => group.tokens.length)}
|
|
585
610
|
groupContent={(index) => (
|
|
586
|
-
|
|
587
|
-
|
|
611
|
+
// Sticky section label: a near-opaque, blurred, widget-glass-colored
|
|
612
|
+
// band — rows dissolve under it while scrolling instead
|
|
613
|
+
// of showing through, and the band still picks up a hint
|
|
614
|
+
// of the page glow like the glass around it.
|
|
615
|
+
<div className="backdrop-blur-xl bg-gradient-to-r from-t_bg_secondary/40 to-t_bg_secondary/40 bg-t_bg_primary bg-opacity-75">
|
|
616
|
+
<p className="pb-3 text-[10px] font-medium leading-4 tracking-[0.015em] text-t_text_primary text-opacity-30 text-left">
|
|
588
617
|
{tokenGroups[index]?.label}
|
|
589
618
|
</p>
|
|
590
619
|
{!tokenGroups[index]?.tokens.length && (
|
|
591
|
-
<div className="text-center py-1 text-sm text-t_text_primary text-opacity-50">
|
|
620
|
+
<div className="text-center py-1 pb-3 text-sm text-t_text_primary text-opacity-50">
|
|
592
621
|
No token found
|
|
593
622
|
</div>
|
|
594
623
|
)}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { ChevronDownIcon } from "@src/assets/icons";
|
|
2
|
-
import {
|
|
2
|
+
import { TokenLogoWithChain } from "@src/components";
|
|
3
3
|
import { useSwapContext } from "@src/context";
|
|
4
|
-
import { Chain, Token
|
|
5
|
-
import {
|
|
6
|
-
import { useMemo, useState } from "react";
|
|
4
|
+
import { Chain, Token } from "@src/models";
|
|
5
|
+
import { useMemo } from "react";
|
|
7
6
|
import { SwapPanelType } from "../../index";
|
|
8
|
-
import { TokenPicker } from "./TokenPicker";
|
|
9
7
|
|
|
10
8
|
type Props = {
|
|
11
9
|
chain: Chain | undefined;
|
|
@@ -17,19 +15,10 @@ type Props = {
|
|
|
17
15
|
export const TokenPanel = ({ chain, token, type, className }: Props) => {
|
|
18
16
|
const {
|
|
19
17
|
bridgeUI,
|
|
20
|
-
srcChain,
|
|
21
|
-
dstChain,
|
|
22
|
-
srcChainTokensOptions,
|
|
23
|
-
srcChainOtherTokensOptions,
|
|
24
|
-
dstChainTokensOptions,
|
|
25
|
-
dstChainOtherTokensOptions,
|
|
26
|
-
setSrcToken,
|
|
27
|
-
setSrcChain,
|
|
28
|
-
supportedChains,
|
|
29
|
-
setDstToken,
|
|
30
|
-
setDstChain,
|
|
31
18
|
dstTokenLocked,
|
|
32
19
|
srcTokenLocked,
|
|
20
|
+
dstChainLocked,
|
|
21
|
+
setActivePicker,
|
|
33
22
|
} = useSwapContext();
|
|
34
23
|
|
|
35
24
|
const integratorLockedDestinationToken = useMemo(
|
|
@@ -42,120 +31,65 @@ export const TokenPanel = ({ chain, token, type, className }: Props) => {
|
|
|
42
31
|
[type, srcTokenLocked],
|
|
43
32
|
);
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
() => (type === "source" ? srcChainTokensOptions : dstChainTokensOptions),
|
|
49
|
-
[dstChainTokensOptions, srcChainTokensOptions, type],
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const pickerDisabled = useMemo(
|
|
34
|
+
// In bridge mode the destination token is derived from the source token,
|
|
35
|
+
// so the destination panel picks a chain instead of a token.
|
|
36
|
+
const bridgeDestination = useMemo(
|
|
53
37
|
() => type === "destination" && bridgeUI,
|
|
54
38
|
[bridgeUI, type],
|
|
55
39
|
);
|
|
56
40
|
|
|
57
|
-
const otherTokensWithBalances = useMemo(
|
|
58
|
-
() =>
|
|
59
|
-
type === "source"
|
|
60
|
-
? srcChainOtherTokensOptions
|
|
61
|
-
: dstChainOtherTokensOptions,
|
|
62
|
-
[dstChainOtherTokensOptions, srcChainOtherTokensOptions, type],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const onTokenSelect = (token: TokenOption) => {
|
|
66
|
-
const newChain = supportedChains.find(
|
|
67
|
-
(chain) => chain.chainId === token.chainId,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (type === "source") {
|
|
71
|
-
setSrcToken(token);
|
|
72
|
-
setSrcChain(newChain);
|
|
73
|
-
// The new source chain cannot reach the current destination —
|
|
74
|
-
// reset the destination so the user picks a reachable one.
|
|
75
|
-
if (
|
|
76
|
-
dstChain &&
|
|
77
|
-
!isChainConnectionSupported(newChain, dstChain.chainId, bridgeUI)
|
|
78
|
-
) {
|
|
79
|
-
setDstChain(undefined);
|
|
80
|
-
setDstToken(undefined);
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
setDstToken(token);
|
|
84
|
-
setDstChain(newChain);
|
|
85
|
-
// The current source cannot reach the new destination chain —
|
|
86
|
-
// reset the source so the user picks a reachable one.
|
|
87
|
-
if (
|
|
88
|
-
srcChain &&
|
|
89
|
-
newChain &&
|
|
90
|
-
!isChainConnectionSupported(srcChain, newChain.chainId, bridgeUI)
|
|
91
|
-
) {
|
|
92
|
-
setSrcChain(undefined);
|
|
93
|
-
setSrcToken(undefined);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
setTokenPickerModalOpen(false);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
41
|
const locked =
|
|
100
|
-
|
|
42
|
+
(bridgeDestination && dstChainLocked) ||
|
|
101
43
|
integratorLockedDestinationToken ||
|
|
102
44
|
integratorLockedSourceToken;
|
|
103
45
|
|
|
104
46
|
return (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
disabled={locked}
|
|
50
|
+
onClick={() => {
|
|
51
|
+
setActivePicker(type);
|
|
52
|
+
}}
|
|
53
|
+
className={`flex items-center gap-1 shrink-0 rounded-lg ${
|
|
54
|
+
locked ? "" : "hover:opacity-80 transition-opacity"
|
|
55
|
+
} ${className || ""}`}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center gap-3">
|
|
58
|
+
<p
|
|
59
|
+
className={`text-sm font-medium leading-6 tracking-[0.01em] text-t_text_primary ${
|
|
60
|
+
token ? "" : "opacity-60"
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{token
|
|
64
|
+
? token.symbol
|
|
65
|
+
: bridgeDestination && chain
|
|
66
|
+
? chain.displayName
|
|
67
|
+
: "Select"}
|
|
68
|
+
</p>
|
|
69
|
+
{token ? (
|
|
70
|
+
<TokenLogoWithChain
|
|
71
|
+
token={token}
|
|
72
|
+
chain={chain}
|
|
73
|
+
tokenLogoClassName="w-10 h-10"
|
|
74
|
+
generatedLogoClassName="w-10 h-10 text-[16px]"
|
|
75
|
+
chainLogoClassName="w-[18px]"
|
|
117
76
|
/>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
setTokenPickerModalOpen(true);
|
|
126
|
-
}}
|
|
127
|
-
className={`flex items-center gap-1 shrink-0 rounded-lg ${
|
|
128
|
-
locked ? "" : "hover:opacity-80 transition-opacity"
|
|
129
|
-
} ${className || ""}`}
|
|
130
|
-
>
|
|
131
|
-
<div className="flex items-center gap-3">
|
|
132
|
-
<p
|
|
133
|
-
className={`text-sm font-medium leading-6 tracking-[0.01em] text-t_text_primary ${
|
|
134
|
-
token ? "" : "opacity-60"
|
|
135
|
-
}`}
|
|
136
|
-
>
|
|
137
|
-
{token
|
|
138
|
-
? token.symbol
|
|
139
|
-
: type === "destination" && bridgeUI
|
|
140
|
-
? "-"
|
|
141
|
-
: "Select"}
|
|
142
|
-
</p>
|
|
143
|
-
{token && (
|
|
144
|
-
<TokenLogoWithChain
|
|
145
|
-
token={token}
|
|
146
|
-
chain={chain}
|
|
147
|
-
tokenLogoClassName="w-10 h-10"
|
|
148
|
-
generatedLogoClassName="w-10 h-10 text-[16px]"
|
|
149
|
-
chainLogoClassName="w-[18px]"
|
|
77
|
+
) : (
|
|
78
|
+
bridgeDestination &&
|
|
79
|
+
chain && (
|
|
80
|
+
<img
|
|
81
|
+
src={chain.image}
|
|
82
|
+
alt={chain.displayName}
|
|
83
|
+
className="w-10 h-10 rounded-full"
|
|
150
84
|
/>
|
|
151
|
-
)
|
|
152
|
-
</div>
|
|
153
|
-
{!locked && (
|
|
154
|
-
<div className="flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 [&_svg]:w-3 [&_svg]:h-3">
|
|
155
|
-
<ChevronDownIcon />
|
|
156
|
-
</div>
|
|
85
|
+
)
|
|
157
86
|
)}
|
|
158
|
-
</
|
|
159
|
-
|
|
87
|
+
</div>
|
|
88
|
+
{!locked && (
|
|
89
|
+
<div className="flex items-center justify-center w-6 h-6 fill-t_text_primary opacity-50 [&_svg]:w-3 [&_svg]:h-3">
|
|
90
|
+
<ChevronDownIcon />
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</button>
|
|
160
94
|
);
|
|
161
95
|
};
|
|
@@ -27,7 +27,7 @@ export const SwapPanel = ({ type }: Props) => {
|
|
|
27
27
|
<div
|
|
28
28
|
className={`flex flex-col gap-2 px-4 sm:px-6 py-4 rounded-2xl bg-t_bg_secondary bg-opacity-60 ${
|
|
29
29
|
type === "source"
|
|
30
|
-
? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5 border border-solid border-t_text_primary border-opacity-
|
|
30
|
+
? "bg-gradient-to-r from-t_bg_tertiary/5 to-t_bg_tertiary/5 border border-solid border-t_text_primary border-opacity-25"
|
|
31
31
|
: "bg-gradient-to-r from-t_bg_tertiary/[0.03] to-t_bg_tertiary/[0.03]"
|
|
32
32
|
}`}
|
|
33
33
|
>
|
|
@@ -3,6 +3,7 @@ import { useSwapContext } from "@src/context";
|
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
import { Header } from "./Header";
|
|
5
5
|
import { HistoryView } from "./HistoryView";
|
|
6
|
+
import { PickerView } from "./PickerView";
|
|
6
7
|
import { SettingsView } from "./SettingsView";
|
|
7
8
|
import { SwapView } from "./SwapView";
|
|
8
9
|
import { UnifiedRoute } from "@src/models";
|
|
@@ -12,7 +13,7 @@ type Props = {
|
|
|
12
13
|
onClose: () => void;
|
|
13
14
|
};
|
|
14
15
|
export const Swap = ({ onClose }: Props) => {
|
|
15
|
-
const { tab } = useSwapContext();
|
|
16
|
+
const { tab, activePicker } = useSwapContext();
|
|
16
17
|
|
|
17
18
|
const view = useMemo(() => {
|
|
18
19
|
return tab === "Settings" ? (
|
|
@@ -26,10 +27,23 @@ export const Swap = ({ onClose }: Props) => {
|
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<div
|
|
29
|
-
className={`relative flex flex-col gap-6 p-
|
|
30
|
+
className={`relative flex flex-col gap-6 p-4 sm:p-8 text-t_text_primary bg-gradient-to-r from-t_bg_tertiary/[0.01] to-t_bg_tertiary/[0.01] bg-t_bg_secondary bg-opacity-50 backdrop-blur-[48px] rounded-[32px] border border-solid border-t_text_primary border-opacity-10 flex-1`}
|
|
30
31
|
>
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
{/* On sm+ the picker replaces the header and the current view in
|
|
33
|
+
place; below sm it renders as a bottom sheet over them. The
|
|
34
|
+
replaced content stays in the layout (visibility only), so it keeps
|
|
35
|
+
defining the widget height for the picker to fill. */}
|
|
36
|
+
<div className="relative flex flex-col flex-1 min-h-0">
|
|
37
|
+
{activePicker && <PickerView />}
|
|
38
|
+
<div
|
|
39
|
+
className={`flex flex-col gap-6 flex-1 min-h-0 ${
|
|
40
|
+
activePicker ? "sm:invisible" : ""
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
<Header onClose={onClose} />
|
|
44
|
+
<div className="flex flex-col flex-1 min-h-0">{view}</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
33
47
|
<PoweredBy />
|
|
34
48
|
</div>
|
|
35
49
|
);
|