@xswap-link/sdk 0.10.5 → 0.10.7

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 (54) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/index.d.mts +46 -12
  4. package/dist/index.d.ts +46 -12
  5. package/dist/index.global.js +5290 -261
  6. package/dist/index.js +9 -9
  7. package/dist/index.mjs +9 -9
  8. package/package.json +13 -2
  9. package/src/assets/icons/StarsIcon.tsx +18 -0
  10. package/src/assets/icons/index.ts +1 -0
  11. package/src/components/AllWalletsConfig/index.tsx +40 -0
  12. package/src/components/Swap/HistoryView/index.tsx +28 -5
  13. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +73 -47
  14. package/src/components/Swap/SwapView/SwapButton/index.tsx +45 -10
  15. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +20 -2
  16. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +124 -4
  17. package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +71 -0
  18. package/src/components/Swap/SwapView/SwapPanel/index.tsx +4 -0
  19. package/src/components/Swap/SwapView/WalletPicker/index.tsx +63 -28
  20. package/src/components/Swap/index.tsx +2 -2
  21. package/src/components/TxConfigForm/index.tsx +12 -34
  22. package/src/components/TxDataCard/TxDataCardUI/index.tsx +16 -10
  23. package/src/components/TxDataCard/index.tsx +1 -0
  24. package/src/components/TxWidgetWC/index.tsx +4 -0
  25. package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
  26. package/src/components/index.ts +1 -0
  27. package/src/config/wagmiConfig.ts +1 -6
  28. package/src/constants/index.ts +14 -0
  29. package/src/context/HistoryProvider.tsx +121 -25
  30. package/src/context/SwapProvider.tsx +104 -48
  31. package/src/context/TransactionProvider.tsx +60 -1
  32. package/src/context/TxUIWrapper.tsx +187 -71
  33. package/src/context/WalletProvider.tsx +108 -0
  34. package/src/contracts/addresses.ts +4 -0
  35. package/src/contracts/idl/jupiter.ts +2879 -0
  36. package/src/models/Ecosystem.ts +1 -0
  37. package/src/models/Route.ts +14 -3
  38. package/src/models/SolanaTransaction.ts +38 -0
  39. package/src/models/SolanaWeb3Network.ts +5 -0
  40. package/src/models/TxUIWrapper.ts +8 -4
  41. package/src/models/index.ts +1 -0
  42. package/src/models/payloads/GetBalancesPayload.ts +2 -1
  43. package/src/models/payloads/GetSolanaRoutePayload.ts +8 -0
  44. package/src/models/payloads/ModalIntegrationPayload.ts +1 -0
  45. package/src/models/payloads/WidgetIntegrationPayload.ts +1 -0
  46. package/src/models/payloads/index.ts +1 -0
  47. package/src/services/api.ts +21 -3
  48. package/src/services/integrations/transactions.ts +2 -0
  49. package/src/services/solana/jupiter/extract-swap-data.ts +183 -0
  50. package/src/services/solana/jupiter/get-events.ts +37 -0
  51. package/src/services/solana/jupiter/instruction-parser.ts +217 -0
  52. package/src/services/solana/jupiter/utils.ts +37 -0
  53. package/src/utils/strings.ts +16 -0
  54. package/test/context/SwapProvider.test.tsx +6 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xswap-link/sdk",
3
- "version": "0.10.5",
3
+ "version": "0.10.7",
4
4
  "description": "JavaScript SDK for XSwap platform",
5
5
  "homepage": "https://github.com/xswap-link/xswap-sdk",
6
6
  "repository": {
@@ -29,23 +29,34 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
+ "@coral-xyz/anchor": "^0.29.0",
32
33
  "@ethersproject/providers": "^5.7.2",
33
34
  "@fortawesome/fontawesome-svg-core": "^6.4.2",
34
35
  "@fortawesome/free-regular-svg-icons": "^6.4.2",
35
36
  "@fortawesome/free-solid-svg-icons": "^6.4.2",
36
37
  "@fortawesome/react-fontawesome": "^0.2.0",
37
38
  "@r2wc/core": "^1.1.0",
39
+ "@solana/spl-token": "^0.4.13",
40
+ "@solana/wallet-adapter-react": "^0.15.35",
41
+ "@solana/wallet-adapter-wallets": "^0.19.32",
42
+ "@solana/web3.js": "^1.98.0",
38
43
  "@tanstack/react-query": "^5.64.2",
39
44
  "@wagmi/connectors": "^5.7.5",
40
45
  "@wagmi/core": "^2.16.3",
41
46
  "async-retry": "^1.3.3",
42
47
  "bignumber.js": "4.0.4",
48
+ "crypto": "npm:crypto-browserify@latest",
43
49
  "date-fns": "^3.6.0",
44
50
  "ethers": "5.7.2",
51
+ "http": "npm:http-browserify@latest",
52
+ "https": "npm:https-browserify@latest",
45
53
  "notistack": "^3.0.1",
46
54
  "react-error-boundary": "^4.1.0",
47
55
  "react-virtuoso": "^4.7.12",
48
- "viem": "~2.22.12",
56
+ "stream": "npm:stream-browserify@latest",
57
+ "url": "npm:url-browserify@latest",
58
+ "zlib": "npm:zlib-browserify@latest",
59
+ "viem": "~2.29.1",
49
60
  "wagmi": "^2.14.9"
50
61
  },
51
62
  "devDependencies": {
@@ -0,0 +1,18 @@
1
+ export const StarsIcon = () => {
2
+ return (
3
+ <svg
4
+ width="16"
5
+ height="16"
6
+ viewBox="0 0 16 16"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <path
11
+ fill-rule="evenodd"
12
+ clip-rule="evenodd"
13
+ d="M5.89761 2.45159L5.12155 4.38335C4.92404 4.875 4.56316 5.26745 4.11108 5.48225L2.33478 6.32623L4.11108 7.17021C4.56316 7.38501 4.92404 7.77746 5.12155 8.26911L5.89761 10.2009L6.67367 8.26911C6.87118 7.77746 7.23206 7.38501 7.68414 7.17021L9.46044 6.32623L7.68414 5.48225C7.23206 5.26745 6.87118 4.875 6.67367 4.38335L5.89761 2.45159ZM11.6352 7.3349C11.4478 7.35272 11.2681 7.47472 11.1863 7.70089L10.5417 9.48369C10.4927 9.61935 10.3985 9.72999 10.2779 9.79365L8.66756 10.6438C8.26644 10.8556 8.26644 11.477 8.66756 11.6888L10.2779 12.539C10.3985 12.6026 10.4927 12.7133 10.5417 12.8489L11.1863 14.6317C11.3639 15.1228 12.0028 15.1228 12.1804 14.6317L12.825 12.8489C12.874 12.7133 12.9682 12.6026 13.0888 12.539L14.6992 11.6888C15.1003 11.477 15.1003 10.8556 14.6992 10.6438L13.0888 9.79365C12.9682 9.72999 12.874 9.61935 12.825 9.48369L12.1804 7.70089C12.0846 7.43603 11.8546 7.31403 11.6352 7.3349ZM8.11177 8.23467C7.90628 8.33231 7.74225 8.51069 7.65247 8.73417L6.71328 11.072C6.40235 11.8459 5.39287 11.846 5.08194 11.072L4.14275 8.73417C4.05297 8.51069 3.88894 8.33231 3.68344 8.23467L1.53375 7.21328C0.822083 6.87514 0.822082 5.77732 1.53375 5.43918L3.68344 4.41779C3.88894 4.32015 4.05297 4.14177 4.14275 3.91829L5.08194 1.58047C5.39287 0.806512 6.40235 0.806511 6.71328 1.58047L7.65247 3.91829C7.74225 4.14177 7.90628 4.32015 8.11177 4.41779L10.2615 5.43918C10.6624 5.6297 10.8375 6.06138 10.7866 6.46247C10.7472 6.77325 10.5722 7.06566 10.2615 7.21328L8.11177 8.23467ZM11.6834 9.50062L11.5358 9.90876C11.3886 10.3157 11.1062 10.6477 10.7444 10.8386L10.1237 11.1663L10.7444 11.494C11.1062 11.6849 11.3886 12.0169 11.5358 12.4239L11.6834 12.832L11.8309 12.4239C11.9781 12.0169 12.2606 11.6849 12.6223 11.494L13.243 11.1663L12.6223 10.8386C12.2606 10.6477 11.9781 10.3157 11.8309 9.90876L11.6834 9.50062Z"
14
+ fill="white"
15
+ ></path>
16
+ </svg>
17
+ );
18
+ };
@@ -41,3 +41,4 @@ export * from "./XMarkIcon";
41
41
  export * from "./XPowerIcon";
42
42
  export * from "./XSwapBadgeIcon";
43
43
  export * from "./XSwapLogo";
44
+ export * from "./StarsIcon";
@@ -0,0 +1,40 @@
1
+ import {
2
+ ConnectionProvider,
3
+ WalletProvider as SolanaWalletProvider,
4
+ } from "@solana/wallet-adapter-react";
5
+ import {
6
+ PhantomWalletAdapter,
7
+ SolflareWalletAdapter,
8
+ } from "@solana/wallet-adapter-wallets";
9
+ import { clusterApiUrl } from "@solana/web3.js";
10
+ import { SolanaWeb3Network } from "@src/models";
11
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
12
+ import { ReactNode, useMemo } from "react";
13
+ import { Config, WagmiProvider } from "wagmi";
14
+
15
+ const queryClient = new QueryClient();
16
+
17
+ export const AllWalletsConfig = ({
18
+ wagmiConfig,
19
+ children,
20
+ }: {
21
+ wagmiConfig: Config;
22
+ children: ReactNode;
23
+ }) => {
24
+ const solanaEndpoint = clusterApiUrl(SolanaWeb3Network.MAINNET);
25
+ const solanaWallets = useMemo(
26
+ () => [new PhantomWalletAdapter(), new SolflareWalletAdapter()],
27
+ [],
28
+ );
29
+ return (
30
+ <WagmiProvider config={wagmiConfig}>
31
+ <QueryClientProvider client={queryClient}>
32
+ <ConnectionProvider endpoint={solanaEndpoint}>
33
+ <SolanaWalletProvider wallets={solanaWallets} autoConnect>
34
+ {children}
35
+ </SolanaWalletProvider>
36
+ </ConnectionProvider>
37
+ </QueryClientProvider>
38
+ </WagmiProvider>
39
+ );
40
+ };
@@ -1,14 +1,32 @@
1
1
  import { Spinner, TxDataCard } from "@src/components";
2
2
  import { useHistory } from "@src/context";
3
+ import { useWallet } from "@src/context/WalletProvider";
3
4
  import { useMemo } from "react";
4
5
  import { Virtuoso } from "react-virtuoso";
5
6
 
6
7
  export const HistoryView = () => {
7
- const { history, historyLoadedOnce } = useHistory();
8
+ const { history, historyLoadedOnce, solanaHistory, solanaHistoryLoadedOnce } =
9
+ useHistory();
10
+ const {
11
+ solana: { address: solanaAddress },
12
+ } = useWallet();
13
+
14
+ const unifiedHistory = useMemo(() => {
15
+ const combinedHistory = [
16
+ ...history,
17
+ ...(solanaAddress ? solanaHistory : []),
18
+ ];
19
+ return combinedHistory.sort((a, b) => {
20
+ return b.timestamp - a.timestamp;
21
+ });
22
+ }, [history, solanaHistory, solanaAddress]);
8
23
 
9
24
  const view = useMemo(() => {
10
- if (!history || history.length === 0) {
11
- return historyLoadedOnce ? (
25
+ if (!unifiedHistory || unifiedHistory.length === 0) {
26
+ const isHistoryLoaded =
27
+ historyLoadedOnce && (solanaAddress ? solanaHistoryLoadedOnce : true);
28
+
29
+ return isHistoryLoaded ? (
12
30
  <div className="text-center py-4 text-[#9e9e9e]">
13
31
  Your history is empty
14
32
  </div>
@@ -21,14 +39,19 @@ export const HistoryView = () => {
21
39
 
22
40
  return (
23
41
  <Virtuoso
24
- data={history}
42
+ data={unifiedHistory}
25
43
  style={{ height: "532px" }}
26
44
  itemContent={(_, transaction) => {
27
45
  return <TxDataCard transaction={transaction} />;
28
46
  }}
29
47
  />
30
48
  );
31
- }, [history, historyLoadedOnce]);
49
+ }, [
50
+ historyLoadedOnce,
51
+ solanaHistoryLoadedOnce,
52
+ unifiedHistory,
53
+ solanaAddress,
54
+ ]);
32
55
 
33
56
  return <div className="flex-1 overflow-hidden">{view}</div>;
34
57
  };
@@ -20,6 +20,8 @@ import { ArrowIcon } from "./ArrowIcon";
20
20
  import { Header } from "./Header";
21
21
  import { Steps } from "./Steps";
22
22
  import { SwapPanel } from "./SwapPanel";
23
+ import { useWallet } from "@solana/wallet-adapter-react";
24
+ import { VersionedTransaction } from "@solana/web3.js";
23
25
 
24
26
  type Props = {
25
27
  onCloseClick: () => void;
@@ -54,7 +56,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
54
56
  evm: { signer },
55
57
  },
56
58
  } = useNetworks();
57
-
59
+ const wallet = useWallet();
58
60
  const evmContractApi = useEvmContractApi();
59
61
 
60
62
  const trackTransaction = useCallback(
@@ -120,8 +122,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
120
122
  transactionData.messageId = messageId;
121
123
 
122
124
  const xSwapRouterOnDestination = new ethers.Contract(
123
- // @ts-ignore
124
- ADDRESSES[dstChain?.chainId]?.[contractKey],
125
+ ADDRESSES[dstChain!.chainId]![contractKey]!,
125
126
  contractKey === "XSwapRouter"
126
127
  ? XSwapRouterAbi
127
128
  : CustomXSwapRouterAbi,
@@ -186,64 +187,88 @@ export const TxOverview = ({ onCloseClick }: Props) => {
186
187
 
187
188
  const enqueueSwapTx = useCallback(async () => {
188
189
  if (srcToken && route) {
189
- const enqueueSwap = () => {
190
+ if (route.ecosystem === Ecosystem.EVM) {
191
+ const enqueueSwap = () => {
192
+ enqueueTransaction({
193
+ executeTransaction: () => {
194
+ if (!route.transactions.swap || !signer) {
195
+ return;
196
+ }
197
+
198
+ return evmContractApi.executeCalldata(
199
+ signer,
200
+ route.transactions.swap,
201
+ );
202
+ },
203
+ txStatus: TxStatus.SIGNING_TX,
204
+ handlers: {
205
+ onSuccess: (data) => {
206
+ setTxStatus(TxStatus.COMPLETED);
207
+ refreshBalance();
208
+ trackTransaction(data);
209
+ if (
210
+ srcChain?.chainId &&
211
+ dstChain?.chainId &&
212
+ srcChain?.chainId !== dstChain?.chainId
213
+ ) {
214
+ renderTxStatus(
215
+ srcChain?.chainId,
216
+ data.transactionHash,
217
+ dstToken?.address,
218
+ dstValueWei,
219
+ );
220
+ }
221
+ },
222
+ },
223
+ });
224
+ };
225
+
226
+ if (
227
+ srcToken.address !== constants.AddressZero &&
228
+ route.transactions.approve
229
+ ) {
230
+ setTxNeedsApproval(true);
231
+ enqueueTransaction({
232
+ executeTransaction: () => {
233
+ if (!route.transactions.approve || !signer) {
234
+ return;
235
+ }
236
+ return evmContractApi.executeCalldata(
237
+ signer,
238
+ route.transactions.approve,
239
+ );
240
+ },
241
+ txStatus: TxStatus.SIGNING_APPROVAL,
242
+ handlers: {
243
+ onSuccess: () => enqueueSwap(),
244
+ },
245
+ });
246
+ } else {
247
+ setTxNeedsApproval(false);
248
+ enqueueSwap();
249
+ }
250
+ } else if (route.ecosystem === Ecosystem.SOLANA) {
190
251
  enqueueTransaction({
191
252
  executeTransaction: () => {
192
- if (!route.transactions.swap || !signer) {
253
+ if (!route.swapTransaction || !wallet) {
193
254
  return;
194
255
  }
195
256
 
196
- return evmContractApi.executeCalldata(
197
- signer,
198
- route.transactions.swap,
257
+ const transaction = VersionedTransaction.deserialize(
258
+ Buffer.from(route.swapTransaction, "base64"),
199
259
  );
260
+
261
+ return wallet.signTransaction?.(transaction);
200
262
  },
201
263
  txStatus: TxStatus.SIGNING_TX,
202
264
  handlers: {
203
- onSuccess: (data) => {
265
+ onSuccess: () => {
204
266
  setTxStatus(TxStatus.COMPLETED);
205
267
  refreshBalance();
206
- trackTransaction(data);
207
- if (
208
- srcChain?.chainId &&
209
- dstChain?.chainId &&
210
- srcChain?.chainId !== dstChain?.chainId
211
- ) {
212
- renderTxStatus(
213
- srcChain?.chainId,
214
- data.transactionHash,
215
- dstToken?.address,
216
- dstValueWei,
217
- );
218
- }
219
268
  },
220
269
  },
270
+ network: Ecosystem.SOLANA,
221
271
  });
222
- };
223
-
224
- if (
225
- srcToken.address !== constants.AddressZero &&
226
- route.transactions.approve
227
- ) {
228
- setTxNeedsApproval(true);
229
- enqueueTransaction({
230
- executeTransaction: () => {
231
- if (!route.transactions.approve || !signer) {
232
- return;
233
- }
234
- return evmContractApi.executeCalldata(
235
- signer,
236
- route.transactions.approve,
237
- );
238
- },
239
- txStatus: TxStatus.SIGNING_APPROVAL,
240
- handlers: {
241
- onSuccess: () => enqueueSwap(),
242
- },
243
- });
244
- } else {
245
- setTxNeedsApproval(false);
246
- enqueueSwap();
247
272
  }
248
273
  }
249
274
  }, [
@@ -260,6 +285,7 @@ export const TxOverview = ({ onCloseClick }: Props) => {
260
285
  dstToken?.address,
261
286
  dstValueWei,
262
287
  setTxNeedsApproval,
288
+ wallet,
263
289
  ]);
264
290
 
265
291
  return (
@@ -1,10 +1,11 @@
1
1
  import { Modal } from "@src/components/Modal";
2
2
  import { useSwapContext, useTxUIWrapper } from "@src/context";
3
+ import { useWallet } from "@src/context/WalletProvider";
3
4
  import { ADDRESSES } from "@src/contracts";
4
5
  import { specialApprovalTokens } from "@src/contracts/specialApprovalTokens";
5
6
  import useNetworks from "@src/hooks/networkManagement/useNetworks";
6
7
  import { useERC20Token } from "@src/hooks/useERC20Token";
7
- import { TxStatus } from "@src/models";
8
+ import { Ecosystem, TxStatus } from "@src/models";
8
9
  import { safeBigNumberFrom } from "@src/utils";
9
10
  import { BigNumber } from "ethers";
10
11
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -20,7 +21,6 @@ type Button = {
20
21
  export const SwapButton = () => {
21
22
  const { address, chainId } = useAccount();
22
23
  const { switchChainAsync } = useSwitchChain();
23
- const [connectWalletModalOpen, setConnectWalletModalOpen] = useState(false);
24
24
  const { integrationConfig } = useSwapContext();
25
25
 
26
26
  const {
@@ -52,6 +52,14 @@ export const SwapButton = () => {
52
52
  networks: { evm },
53
53
  } = useNetworks();
54
54
 
55
+ const {
56
+ openEVMWalletModal,
57
+ setOpenEVMWalletModal,
58
+ openSolanaWalletModal,
59
+ setOpenSolanaWalletModal,
60
+ solana,
61
+ } = useWallet();
62
+
55
63
  const { allowance, approve } = useERC20Token({
56
64
  rpcChainId: srcChainId,
57
65
  currentChainId: evm.chainId,
@@ -111,7 +119,7 @@ export const SwapButton = () => {
111
119
  };
112
120
  }
113
121
 
114
- if (!address) {
122
+ if (srcChain?.ecosystem !== Ecosystem.SOLANA && !address) {
115
123
  return {
116
124
  text:
117
125
  integrationConfig.defaultWalletPicker || hasOnConnectWalletCallback
@@ -119,7 +127,7 @@ export const SwapButton = () => {
119
127
  : "Connect Wallet First",
120
128
  fn: () => {
121
129
  if (integrationConfig.defaultWalletPicker) {
122
- setConnectWalletModalOpen(true);
130
+ setOpenEVMWalletModal(true);
123
131
  } else if (hasOnConnectWalletCallback) {
124
132
  onConnectWallet();
125
133
  }
@@ -147,8 +155,19 @@ export const SwapButton = () => {
147
155
  disabled: true,
148
156
  };
149
157
  }
150
-
151
- if (chainId?.toString() !== srcChainId) {
158
+ if (srcChain?.ecosystem === Ecosystem.SOLANA && !solana.address) {
159
+ return {
160
+ text: "Connect Solana Wallet",
161
+ fn: () => {
162
+ setOpenSolanaWalletModal(true);
163
+ },
164
+ disabled: false,
165
+ };
166
+ }
167
+ if (
168
+ srcChain?.ecosystem !== Ecosystem.SOLANA &&
169
+ chainId?.toString() !== srcChainId
170
+ ) {
152
171
  return {
153
172
  text: "Change Chain",
154
173
  fn: async () => await switchChainAsync({ chainId: Number(srcChainId) }),
@@ -218,11 +237,13 @@ export const SwapButton = () => {
218
237
  }, [
219
238
  isAsyncDataLoaded,
220
239
  error,
240
+ srcChain?.ecosystem,
221
241
  address,
222
242
  srcChainId,
223
243
  dstChainId,
224
244
  srcTokenAddress,
225
245
  dstTokenAddress,
246
+ solana.address,
226
247
  chainId,
227
248
  srcValueWei,
228
249
  srcTokenBalanceWei,
@@ -230,6 +251,10 @@ export const SwapButton = () => {
230
251
  currentTokenAllowance,
231
252
  bridgeUI,
232
253
  integrationConfig.defaultWalletPicker,
254
+ hasOnConnectWalletCallback,
255
+ setOpenEVMWalletModal,
256
+ onConnectWallet,
257
+ setOpenSolanaWalletModal,
233
258
  switchChainAsync,
234
259
  enqueueTransaction,
235
260
  approve,
@@ -241,12 +266,22 @@ export const SwapButton = () => {
241
266
 
242
267
  return (
243
268
  <>
244
- {connectWalletModalOpen && (
245
- <Modal onClose={() => setConnectWalletModalOpen(false)}>
246
- <WalletPicker onClose={() => setConnectWalletModalOpen(false)} />
269
+ {openEVMWalletModal && (
270
+ <Modal onClose={() => setOpenEVMWalletModal(false)}>
271
+ <WalletPicker
272
+ ecosystem={Ecosystem.EVM}
273
+ onClose={() => setOpenEVMWalletModal(false)}
274
+ />
275
+ </Modal>
276
+ )}
277
+ {openSolanaWalletModal && (
278
+ <Modal onClose={() => setOpenSolanaWalletModal(false)}>
279
+ <WalletPicker
280
+ ecosystem={Ecosystem.SOLANA}
281
+ onClose={() => setOpenSolanaWalletModal(false)}
282
+ />
247
283
  </Modal>
248
284
  )}
249
-
250
285
  <button
251
286
  type="button"
252
287
  disabled={button.disabled}
@@ -1,4 +1,6 @@
1
+ import { StarsIcon } from "@src/assets/icons";
1
2
  import { Skeleton, TokenLogoWithChain } from "@src/components";
3
+ import { SwapPanelType } from "@src/components/Swap/SwapView";
2
4
  import { useSwapContext } from "@src/context";
3
5
  import { TokenOption } from "@src/models";
4
6
  import { weiToHumanReadable } from "@src/utils";
@@ -11,14 +13,21 @@ type Props = {
11
13
  selectedTokenAddress: string | undefined;
12
14
  selectedChainId: string | undefined;
13
15
  onClick: () => void;
16
+ panelType: SwapPanelType;
14
17
  };
15
18
  export const TokenItem = ({
16
19
  token,
17
20
  selectedTokenAddress,
18
21
  onClick,
19
22
  selectedChainId,
23
+ panelType,
20
24
  }: Props) => {
21
- const { supportedChains, tokenPrices, isFetchingBalances } = useSwapContext();
25
+ const {
26
+ supportedChains,
27
+ tokenPrices,
28
+ isFetchingBalances,
29
+ integrationConfig,
30
+ } = useSwapContext();
22
31
  const { address } = useAccount();
23
32
 
24
33
  const chain = useMemo(
@@ -66,8 +75,17 @@ export const TokenItem = ({
66
75
  />
67
76
  <div className="flex justify-between items-center gap-1 w-full">
68
77
  <div className="text-left">
69
- <div className="text-sm font-medium text-ellipsis text-t_text_primary">
78
+ <div className="text-sm font-medium text-ellipsis text-t_text_primary flex items-center gap-1">
70
79
  {token.name}
80
+ {integrationConfig?.highlightedDstTokens?.includes(token.symbol) &&
81
+ panelType === "destination" && (
82
+ <div
83
+ className={`flex items-center justify-center text-white rounded-full border-t_bg_secondary ${"w-3.5 h-3.5 text-xs"}`}
84
+ title="AI-powered token"
85
+ >
86
+ <StarsIcon />
87
+ </div>
88
+ )}
71
89
  </div>
72
90
  <div className="text-xs text-t_text_primary text-opacity-50 font-medium">
73
91
  {token.symbol}
@@ -13,13 +13,20 @@ import { useSwapContext } from "@src/context";
13
13
  import { ERC20Abi } from "@src/contracts";
14
14
  import { useEvmContractApi } from "@src/hooks";
15
15
  import { Chain, Ecosystem, ImportedTokenData, TokenOption } from "@src/models";
16
- import { isETHAddressValid, isServer, shortAddress } from "@src/utils";
16
+ import {
17
+ isETHAddressValid,
18
+ isServer,
19
+ isSolanaAddressValid,
20
+ shortAddress,
21
+ } from "@src/utils";
17
22
  import { BigNumber, ethers } from "ethers";
18
23
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
19
24
  import { GroupedVirtuoso } from "react-virtuoso";
20
25
  import { QuickPickTokenItem } from "./QuickPickTokenItem";
21
26
  import { TokenItem } from "./TokenItem";
22
27
  import { useAccount } from "wagmi";
28
+ import { Connection, PublicKey } from "@solana/web3.js";
29
+ import { METADATA_PROGRAM_ID } from "@src/constants";
23
30
 
24
31
  type Props = {
25
32
  type: SwapPanelType;
@@ -67,7 +74,11 @@ export const TokenPicker = ({
67
74
  }, [isOpen]);
68
75
 
69
76
  const importCustomToken = useCallback(() => {
70
- if (chain && customTokenData && isETHAddressValid(searchValue)) {
77
+ if (
78
+ chain &&
79
+ customTokenData &&
80
+ (isETHAddressValid(searchValue) || isSolanaAddressValid(searchValue))
81
+ ) {
71
82
  let customTokens = {};
72
83
  if (!isServer) {
73
84
  customTokens = JSON.parse(
@@ -145,14 +156,121 @@ export const TokenPicker = ({
145
156
  }
146
157
  }, [chain, searchValue, evmContractApi, supportedChains]);
147
158
 
159
+ const getCustomSolanaTokenData = useCallback(async () => {
160
+ const nonce = Date.now();
161
+ currentRouteRequestNonce.current = nonce;
162
+
163
+ const rpcUrl = supportedChains.find(
164
+ ({ ecosystem, chainId }) =>
165
+ ecosystem === Ecosystem.SOLANA && chainId === chain?.chainId,
166
+ )?.publicRpcUrls[0];
167
+
168
+ if (!rpcUrl || !searchValue || !isSolanaAddressValid(searchValue)) {
169
+ setCustomTokenData(null);
170
+ setLoadingCustomTokenData(false);
171
+ return;
172
+ }
173
+
174
+ setLoadingCustomTokenData(true);
175
+
176
+ try {
177
+ const connection = new Connection(rpcUrl, "confirmed");
178
+ const mintPublicKey = new PublicKey(searchValue);
179
+ const mintInfo = await connection.getParsedAccountInfo(mintPublicKey);
180
+
181
+ if (
182
+ !mintInfo?.value?.data ||
183
+ !("parsed" in mintInfo.value.data) ||
184
+ mintInfo.value.data.program !== "spl-token"
185
+ ) {
186
+ setCustomTokenData(null);
187
+ return;
188
+ }
189
+ const decimals = mintInfo.value.data.parsed.info.decimals;
190
+
191
+ if (nonce !== currentRouteRequestNonce.current) {
192
+ return;
193
+ }
194
+
195
+ const TOKEN_METADATA_PROGRAM_ID = new PublicKey(METADATA_PROGRAM_ID);
196
+
197
+ const [metadataPDA] = PublicKey.findProgramAddressSync(
198
+ [
199
+ Buffer.from("metadata"),
200
+ TOKEN_METADATA_PROGRAM_ID.toBuffer(),
201
+ mintPublicKey.toBuffer(),
202
+ ],
203
+ TOKEN_METADATA_PROGRAM_ID,
204
+ );
205
+
206
+ const metadataAccount = await connection.getAccountInfo(metadataPDA);
207
+
208
+ if (!metadataAccount) {
209
+ throw new Error("Metadata account not found");
210
+ }
211
+
212
+ const data = Buffer.from(metadataAccount.data);
213
+
214
+ // First byte is key (always 4 for Metadata), then comes actual data
215
+ let offset = 1;
216
+
217
+ // Read update authority - 32 bytes
218
+ offset += 32;
219
+
220
+ // Read mint - 32 bytes
221
+ offset += 32;
222
+
223
+ // Read name
224
+ const nameLen = Math.min(data.readUInt32LE(offset), 32); // Max name length is 32
225
+ offset += 4;
226
+ const name = data
227
+ .slice(offset, offset + nameLen)
228
+ .toString("utf8")
229
+ .replace(/\0/g, "");
230
+ offset += 32; // Name field is always 32 bytes, padded with nulls
231
+
232
+ // Read symbol
233
+ const symbolLen = Math.min(data.readUInt32LE(offset), 10); // Max symbol length is 10
234
+ offset += 4;
235
+ const symbol = data
236
+ .slice(offset, offset + symbolLen)
237
+ .toString("utf8")
238
+ .replace(/\0/g, "");
239
+
240
+ setCustomTokenData({
241
+ address: searchValue,
242
+ name:
243
+ name || `Token ${searchValue.slice(0, 4)}...${searchValue.slice(-4)}`,
244
+ symbol: symbol || searchValue.slice(0, 4),
245
+ decimals,
246
+ });
247
+ } catch (error) {
248
+ console.error("Error fetching Solana token data:", error);
249
+ setCustomTokenData(null);
250
+ } finally {
251
+ if (nonce === currentRouteRequestNonce.current) {
252
+ setLoadingCustomTokenData(false);
253
+ }
254
+ }
255
+ }, [chain, searchValue, supportedChains]);
256
+
148
257
  useEffect(() => {
149
258
  if (
150
259
  !bridgeUI &&
151
- isETHAddressValid(searchValue) &&
152
260
  chain &&
153
261
  !findTokenDataByAddressAndChain(searchValue.toLowerCase(), chain.chainId)
154
262
  ) {
155
- getCustomTokenData();
263
+ if (chain.ecosystem === Ecosystem.EVM && isETHAddressValid(searchValue)) {
264
+ getCustomTokenData();
265
+ } else if (
266
+ chain.ecosystem === Ecosystem.SOLANA &&
267
+ isSolanaAddressValid(searchValue)
268
+ ) {
269
+ getCustomSolanaTokenData();
270
+ } else {
271
+ setLoadingCustomTokenData(false);
272
+ setCustomTokenData(null);
273
+ }
156
274
  } else {
157
275
  setLoadingCustomTokenData(false);
158
276
  setCustomTokenData(null);
@@ -161,6 +279,7 @@ export const TokenPicker = ({
161
279
  bridgeUI,
162
280
  searchValue,
163
281
  getCustomTokenData,
282
+ getCustomSolanaTokenData,
164
283
  findTokenDataByAddressAndChain,
165
284
  chain,
166
285
  ]);
@@ -518,6 +637,7 @@ export const TokenPicker = ({
518
637
  return (
519
638
  allTokens[index] && (
520
639
  <TokenItem
640
+ panelType={type}
521
641
  token={allTokens[index]}
522
642
  selectedChainId={chain?.chainId}
523
643
  selectedTokenAddress={selectedTokenAddress}