|
1 | 1 | import { useWallet, useConnection } from "@solana/wallet-adapter-react"; |
2 | | -import { PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js"; |
| 2 | +import { |
| 3 | + PublicKey, |
| 4 | + TransactionInstruction, |
| 5 | + VersionedTransaction, |
| 6 | + TransactionMessage |
| 7 | +} from "@solana/web3.js"; |
3 | 8 | import { createCloseAccountInstruction } from "@solana/spl-token"; |
4 | 9 | import { toast } from "react-hot-toast"; |
5 | | -import { useState } from "react"; |
| 10 | +import { getTipAccounts, sendTxUsingJito, waitForBundleConfirmation } from "@utils/hooks/useCreateSwapInstructions"; |
| 11 | +import { SystemProgram } from "@solana/web3.js"; |
| 12 | + |
| 13 | +const MAX_ACCOUNTS_PER_TX = 12; // Conservative limit for accounts per transaction |
| 14 | +const BUNDLE_TIP = 1000; // Tip amount for Jito bundles |
| 15 | +const USER_REJECTION_ERROR = 'User rejected the request'; |
6 | 16 |
|
7 | 17 | export const useCloseTokenAccount = () => { |
8 | | - const { publicKey, signTransaction, sendTransaction } = useWallet(); |
| 18 | + const { publicKey, signAllTransactions } = useWallet(); |
9 | 19 | const { connection } = useConnection(); |
10 | 20 |
|
11 | 21 | const closeTokenAccount = async (tokenAccountPubkey: PublicKey): Promise<TransactionInstruction> => { |
12 | | - if (!publicKey || !signTransaction || !tokenAccountPubkey) { |
| 22 | + if (!publicKey || !tokenAccountPubkey) { |
13 | 23 | throw new Error("Wallet not connected or not able to sign transactions or Error with token account address"); |
14 | 24 | } |
15 | 25 |
|
@@ -37,34 +47,82 @@ export const useCloseTokenAccount = () => { |
37 | 47 | return closeInstruction; |
38 | 48 | }; |
39 | 49 |
|
40 | | - const closeTokenAccountsAndSendTransaction = async (instructions: TransactionInstruction[]) => { |
41 | | - if (!publicKey || !signTransaction) { |
42 | | - throw new Error("Wallet not connected or not able to sign transactions"); |
| 50 | + const closeTokenAccountsAndSendTransaction = async ( |
| 51 | + tokenAccounts: PublicKey[], |
| 52 | + setMessage?: (msg: string) => void |
| 53 | + ) => { |
| 54 | + if (!publicKey || !signAllTransactions) { |
| 55 | + throw new Error("Wallet not connected"); |
43 | 56 | } |
44 | 57 |
|
45 | | - const transaction = new Transaction().add(...instructions); |
46 | | - |
47 | 58 | try { |
| 59 | + setMessage?.("Preparing to close token accounts..."); |
| 60 | + const tipAccount = await getTipAccounts(); |
| 61 | + |
| 62 | + // Split token accounts into chunks |
| 63 | + const chunks: PublicKey[][] = []; |
| 64 | + for (let i = 0; i < tokenAccounts.length; i += MAX_ACCOUNTS_PER_TX) { |
| 65 | + chunks.push(tokenAccounts.slice(i, i + MAX_ACCOUNTS_PER_TX)); |
| 66 | + } |
| 67 | + |
| 68 | + console.log(`Split ${tokenAccounts.length} accounts into ${chunks.length} chunks`); |
| 69 | + |
| 70 | + // Create all transactions |
| 71 | + const transactions: VersionedTransaction[] = []; |
48 | 72 | const { blockhash } = await connection.getLatestBlockhash(); |
49 | | - const latestBlockHash = await connection.getLatestBlockhash(); |
50 | | - transaction.recentBlockhash = blockhash; |
51 | | - transaction.feePayer = publicKey; |
52 | | - |
53 | | - const signedTransaction = await signTransaction(transaction); |
54 | | - const signature = await sendTransaction(signedTransaction, connection); |
55 | | - |
56 | | - await connection.confirmTransaction({ |
57 | | - blockhash, |
58 | | - lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, |
59 | | - signature |
60 | | - }); |
61 | | - |
62 | | - console.log("Transaction confirmed with signature:", signature); |
63 | | - toast.success("Transaction confirmed successfully!"); |
64 | | - return signature; |
65 | | - } catch (error) { |
66 | | - console.error("Failed to send transaction:", error); |
67 | | - toast.error("Failed to send transaction. Please try again."); |
| 73 | + |
| 74 | + for (const chunk of chunks) { |
| 75 | + const instructions = await Promise.all( |
| 76 | + chunk.map(account => closeTokenAccount(account)) |
| 77 | + ); |
| 78 | + |
| 79 | + const tipInstruction = SystemProgram.transfer({ |
| 80 | + fromPubkey: publicKey, |
| 81 | + toPubkey: new PublicKey(tipAccount), |
| 82 | + lamports: BUNDLE_TIP, |
| 83 | + }); |
| 84 | + |
| 85 | + const messageV0 = new TransactionMessage({ |
| 86 | + payerKey: publicKey, |
| 87 | + recentBlockhash: blockhash, |
| 88 | + instructions: [...instructions, tipInstruction], |
| 89 | + }).compileToV0Message(); |
| 90 | + |
| 91 | + transactions.push(new VersionedTransaction(messageV0)); |
| 92 | + } |
| 93 | + |
| 94 | + setMessage?.("Please approve the transaction in your wallet..."); |
| 95 | + |
| 96 | + try { |
| 97 | + // Sign all transactions at once |
| 98 | + const signedTransactions = await signAllTransactions(transactions); |
| 99 | + |
| 100 | + // Send transactions as Jito bundles |
| 101 | + for (let i = 0; i < signedTransactions.length; i++) { |
| 102 | + setMessage?.(`Processing chunk ${i + 1}/${signedTransactions.length}...`); |
| 103 | + const signedTx = signedTransactions[i]; |
| 104 | + const serializedTx = signedTx.serialize(); |
| 105 | + const bundleId = await sendTxUsingJito([serializedTx]); |
| 106 | + |
| 107 | + setMessage?.(`Confirming chunk ${i + 1}...`); |
| 108 | + const confirmed = await waitForBundleConfirmation(bundleId); |
| 109 | + if (!confirmed) { |
| 110 | + throw new Error(`Bundle ${bundleId} failed to confirm`); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + toast.success("Successfully closed all token accounts!"); |
| 115 | + return true; |
| 116 | + } catch (error: any) { |
| 117 | + if (error?.message?.includes(USER_REJECTION_ERROR)) { |
| 118 | + toast.error("Transaction rejected by user"); |
| 119 | + return false; |
| 120 | + } |
| 121 | + throw error; |
| 122 | + } |
| 123 | + } catch (error: any) { |
| 124 | + console.error("Failed to close token accounts:", error); |
| 125 | + toast.error(error?.message || "Failed to close token accounts. Please try again."); |
68 | 126 | throw error; |
69 | 127 | } |
70 | 128 | }; |
|
0 commit comments