@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.
- package/.github/workflows/main.yml +1 -1
- package/.github/workflows/publish.yml +10 -10
- package/CHANGELOG.md +13 -0
- package/dist/index.d.mts +39 -8
- package/dist/index.d.ts +39 -8
- package/dist/index.global.js +827 -440
- package/dist/index.js +9 -9
- package/dist/index.mjs +9 -9
- package/package.json +12 -1
- 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/index.tsx +123 -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 +9 -34
- package/src/components/TxDataCard/TxDataCardUI/index.tsx +2 -2
- package/src/components/TxDataCard/index.tsx +7 -2
- 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/index.ts +1 -0
- package/src/services/api.ts +21 -3
- package/src/services/solana/jupiter/extract-swap-data.ts +182 -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.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 } =
|
|
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}
|
|
@@ -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
|
]);
|
|
@@ -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}
|