@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.
- package/.github/workflows/main.yml +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/index.d.mts +46 -12
- package/dist/index.d.ts +46 -12
- package/dist/index.global.js +5290 -261
- package/dist/index.js +9 -9
- package/dist/index.mjs +9 -9
- package/package.json +13 -2
- package/src/assets/icons/StarsIcon.tsx +18 -0
- package/src/assets/icons/index.ts +1 -0
- package/src/components/AllWalletsConfig/index.tsx +40 -0
- package/src/components/Swap/HistoryView/index.tsx +28 -5
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +73 -47
- package/src/components/Swap/SwapView/SwapButton/index.tsx +45 -10
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/TokenItem/index.tsx +20 -2
- package/src/components/Swap/SwapView/SwapPanel/TokenPanel/TokenPicker/index.tsx +124 -4
- package/src/components/Swap/SwapView/SwapPanel/WalletPanel/index.tsx +71 -0
- package/src/components/Swap/SwapView/SwapPanel/index.tsx +4 -0
- package/src/components/Swap/SwapView/WalletPicker/index.tsx +63 -28
- package/src/components/Swap/index.tsx +2 -2
- package/src/components/TxConfigForm/index.tsx +12 -34
- package/src/components/TxDataCard/TxDataCardUI/index.tsx +16 -10
- package/src/components/TxDataCard/index.tsx +1 -0
- package/src/components/TxWidgetWC/index.tsx +4 -0
- package/src/components/TxWidgetWCWrapped/index.tsx +2 -0
- package/src/components/index.ts +1 -0
- package/src/config/wagmiConfig.ts +1 -6
- package/src/constants/index.ts +14 -0
- package/src/context/HistoryProvider.tsx +121 -25
- package/src/context/SwapProvider.tsx +104 -48
- package/src/context/TransactionProvider.tsx +60 -1
- package/src/context/TxUIWrapper.tsx +187 -71
- package/src/context/WalletProvider.tsx +108 -0
- package/src/contracts/addresses.ts +4 -0
- package/src/contracts/idl/jupiter.ts +2879 -0
- package/src/models/Ecosystem.ts +1 -0
- package/src/models/Route.ts +14 -3
- package/src/models/SolanaTransaction.ts +38 -0
- package/src/models/SolanaWeb3Network.ts +5 -0
- package/src/models/TxUIWrapper.ts +8 -4
- package/src/models/index.ts +1 -0
- package/src/models/payloads/GetBalancesPayload.ts +2 -1
- package/src/models/payloads/GetSolanaRoutePayload.ts +8 -0
- package/src/models/payloads/ModalIntegrationPayload.ts +1 -0
- package/src/models/payloads/WidgetIntegrationPayload.ts +1 -0
- package/src/models/payloads/index.ts +1 -0
- package/src/services/api.ts +21 -3
- package/src/services/integrations/transactions.ts +2 -0
- package/src/services/solana/jupiter/extract-swap-data.ts +183 -0
- package/src/services/solana/jupiter/get-events.ts +37 -0
- package/src/services/solana/jupiter/instruction-parser.ts +217 -0
- package/src/services/solana/jupiter/utils.ts +37 -0
- package/src/utils/strings.ts +16 -0
- 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.
|
|
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
|
-
"
|
|
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
|
+
};
|
|
@@ -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 } =
|
|
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 (!
|
|
11
|
-
|
|
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={
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
253
|
+
if (!route.swapTransaction || !wallet) {
|
|
193
254
|
return;
|
|
194
255
|
}
|
|
195
256
|
|
|
196
|
-
|
|
197
|
-
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
245
|
-
<Modal onClose={() =>
|
|
246
|
-
<WalletPicker
|
|
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 {
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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}
|