@xswap-link/sdk 0.10.6 → 0.10.8

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 (47) hide show
  1. package/.github/workflows/main.yml +1 -1
  2. package/.github/workflows/publish.yml +10 -10
  3. package/CHANGELOG.md +13 -0
  4. package/dist/index.d.mts +39 -8
  5. package/dist/index.d.ts +39 -8
  6. package/dist/index.global.js +827 -440
  7. package/dist/index.js +9 -9
  8. package/dist/index.mjs +9 -9
  9. package/package.json +12 -1
  10. package/src/components/AllWalletsConfig/index.tsx +40 -0
  11. package/src/components/Swap/HistoryView/index.tsx +28 -5
  12. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +73 -47
  13. package/src/components/Swap/SwapView/SwapButton/index.tsx +45 -10
  14. package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +123 -4
  15. package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +71 -0
  16. package/src/components/Swap/SwapView/SwapPanel/index.tsx +4 -0
  17. package/src/components/Swap/SwapView/WalletPicker/index.tsx +63 -28
  18. package/src/components/Swap/index.tsx +2 -2
  19. package/src/components/TxConfigForm/index.tsx +9 -34
  20. package/src/components/TxDataCard/TxDataCardUI/index.tsx +2 -2
  21. package/src/components/TxDataCard/index.tsx +7 -2
  22. package/src/components/index.ts +1 -0
  23. package/src/config/wagmiConfig.ts +1 -6
  24. package/src/constants/index.ts +14 -0
  25. package/src/context/HistoryProvider.tsx +121 -25
  26. package/src/context/SwapProvider.tsx +104 -48
  27. package/src/context/TransactionProvider.tsx +60 -1
  28. package/src/context/TxUIWrapper.tsx +187 -71
  29. package/src/context/WalletProvider.tsx +108 -0
  30. package/src/contracts/addresses.ts +4 -0
  31. package/src/contracts/idl/jupiter.ts +2879 -0
  32. package/src/models/Ecosystem.ts +1 -0
  33. package/src/models/Route.ts +14 -3
  34. package/src/models/SolanaTransaction.ts +38 -0
  35. package/src/models/SolanaWeb3Network.ts +5 -0
  36. package/src/models/TxUIWrapper.ts +8 -4
  37. package/src/models/index.ts +1 -0
  38. package/src/models/payloads/GetBalancesPayload.ts +2 -1
  39. package/src/models/payloads/GetSolanaRoutePayload.ts +8 -0
  40. package/src/models/payloads/index.ts +1 -0
  41. package/src/services/api.ts +21 -3
  42. package/src/services/solana/jupiter/extract-swap-data.ts +182 -0
  43. package/src/services/solana/jupiter/get-events.ts +37 -0
  44. package/src/services/solana/jupiter/instruction-parser.ts +217 -0
  45. package/src/services/solana/jupiter/utils.ts +37 -0
  46. package/src/utils/strings.ts +16 -0
  47. 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.6",
3
+ "version": "0.10.8",
4
4
  "description": "JavaScript SDK for XSwap platform",
5
5
  "homepage": "https://github.com/xswap-link/xswap-sdk",
6
6
  "repository": {
@@ -29,22 +29,33 @@
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",
56
+ "stream": "npm:stream-browserify@latest",
57
+ "url": "npm:url-browserify@latest",
58
+ "zlib": "npm:zlib-browserify@latest",
48
59
  "viem": "~2.29.1",
49
60
  "wagmi": "^2.14.9"
50
61
  },
@@ -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}
@@ -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
  ]);
@@ -0,0 +1,71 @@
1
+ import { useWallet } from "@src/context/WalletProvider";
2
+ import { SwapPanelType } from "../..";
3
+ import { Chain } from "@src/models";
4
+ import { useCallback } from "react";
5
+ import { shortAddress } from "@src/utils";
6
+
7
+ type Props = {
8
+ chain: Chain | undefined;
9
+ type: SwapPanelType;
10
+ className?: string;
11
+ };
12
+
13
+ export const WalletPanel = ({ chain, type, className }: Props) => {
14
+ const { setOpenEVMWalletModal, setOpenSolanaWalletModal, evm, solana } =
15
+ useWallet();
16
+ const openWalletModal = useCallback(() => {
17
+ if (!chain) {
18
+ return;
19
+ }
20
+ if (chain.ecosystem === "evm") {
21
+ setOpenEVMWalletModal(true);
22
+ evm.connectWallet();
23
+ } else if (chain.ecosystem === "solana") {
24
+ setOpenSolanaWalletModal(true);
25
+ solana.connectWallet();
26
+ }
27
+ }, [chain, evm, solana, setOpenEVMWalletModal, setOpenSolanaWalletModal]);
28
+ const renderAddress = useCallback(
29
+ (chainEcosystem: string) => {
30
+ if (chainEcosystem === "evm") {
31
+ return shortAddress(evm.address || "");
32
+ } else if (chainEcosystem === "solana") {
33
+ return shortAddress(solana.address || "");
34
+ }
35
+ },
36
+ [evm, solana],
37
+ );
38
+ return (
39
+ <>
40
+ <button
41
+ type="button"
42
+ onClick={() => openWalletModal()}
43
+ className={`${className || ""}`}
44
+ >
45
+ <div className="flex justify-between items-center p-4 gap-8">
46
+ <div className="flex flex-col items-start">
47
+ {type === "source" ? (
48
+ <div className="flex items-center gap-2">
49
+ <p className="text-xs font-medium opacity-60">Sender:</p>
50
+ {chain && (
51
+ <p className="text-sm font-medium">
52
+ {renderAddress(chain.ecosystem)}
53
+ </p>
54
+ )}
55
+ </div>
56
+ ) : (
57
+ <div className="flex items-center gap-2">
58
+ <p className="text-xs font-medium opacity-60">Receiver:</p>
59
+ {chain && (
60
+ <p className="text-sm font-medium">
61
+ {renderAddress(chain.ecosystem)}
62
+ </p>
63
+ )}
64
+ </div>
65
+ )}
66
+ </div>
67
+ </div>
68
+ </button>
69
+ </>
70
+ );
71
+ };
@@ -5,6 +5,7 @@ import { AmountPanel } from "./AmountPanel";
5
5
  import { ChainPanel } from "./ChainPanel";
6
6
  import { MaxPanel } from "./MaxPanel";
7
7
  import { TokenPanel } from "./TokenPanel";
8
+ import { WalletPanel } from "./WalletPanel";
8
9
 
9
10
  type Props = {
10
11
  type: SwapPanelType;
@@ -24,6 +25,9 @@ export const SwapPanel = ({ type }: Props) => {
24
25
 
25
26
  return (
26
27
  <div className="flex flex-col x-border rounded-xl bg-t_bg_tertiary bg-opacity-5">
28
+ <div className="flex border-b border-solid border-t_text_primary border-opacity-10">
29
+ <WalletPanel chain={chain} type={type} className="w-full flex-grow" />
30
+ </div>
27
31
  <div className="flex border-b border-solid border-t_text_primary border-opacity-10">
28
32
  <TokenPanel
29
33
  chain={chain}