|
1 | | -import type { |
2 | | - AddressesByLookupTableAddress, |
3 | | - Instruction, |
4 | | - Signature, |
5 | | - TransactionSigner, |
6 | | -} from "@solana/kit"; |
7 | | -import { |
8 | | - compressTransactionMessageUsingAddressLookupTables, |
9 | | - getBase58Decoder, |
10 | | - signAndSendTransactionMessageWithSigners, |
11 | | -} from "@solana/kit"; |
12 | | -import { createTransaction, getExplorerLink } from "gill"; |
13 | | - |
14 | | -import { useSolanaClient } from "gill-react"; |
15 | | -import { useCallback } from "react"; |
16 | 1 | import { useGrillContext } from "../contexts/grill-context.js"; |
17 | | -import { useKitWallet } from "./use-kit-wallet.js"; |
18 | | - |
19 | | -export type TransactionId = string; |
20 | | - |
21 | | -export type TransactionStatusEvent = { |
22 | | - title: string; |
23 | | - id: TransactionId; |
24 | | -} & ( |
25 | | - | { |
26 | | - type: "error-wallet-not-connected"; |
27 | | - } |
28 | | - | { |
29 | | - type: "preparing"; |
30 | | - } |
31 | | - | { |
32 | | - type: "awaiting-wallet-signature"; |
33 | | - } |
34 | | - | { |
35 | | - type: "waiting-for-confirmation"; |
36 | | - sig: Signature; |
37 | | - explorerLink: string; |
38 | | - } |
39 | | - | { |
40 | | - type: "confirmed"; |
41 | | - sig: Signature; |
42 | | - explorerLink: string; |
43 | | - } |
44 | | - | { |
45 | | - type: "error-transaction-failed"; |
46 | | - errorMessage: string; |
47 | | - sig: Signature; |
48 | | - explorerLink: string; |
49 | | - } |
50 | | -); |
51 | | - |
52 | | -export interface SendTXOptions { |
53 | | - luts?: AddressesByLookupTableAddress; |
54 | | - signers?: TransactionSigner[]; |
55 | | -} |
56 | 2 |
|
57 | | -export type SendTXFunction = ( |
58 | | - name: string, |
59 | | - ixs: readonly Instruction[], |
60 | | - options?: SendTXOptions, |
61 | | -) => Promise<Signature>; |
| 3 | +export type { |
| 4 | + SendTXFunction, |
| 5 | + SendTXOptions, |
| 6 | +} from "../utils/internal/create-send-tx.js"; |
62 | 7 |
|
63 | 8 | /** |
64 | 9 | * Hook that provides a function to send transactions using the modern @solana/kit API |
65 | 10 | * while maintaining compatibility with the wallet adapter. |
66 | 11 | */ |
67 | | -export const useSendTX = (): SendTXFunction => { |
68 | | - const { reloadAccounts, internal_onTransactionStatusEvent } = |
69 | | - useGrillContext(); |
70 | | - const { signer } = useKitWallet(); |
71 | | - const { rpc } = useSolanaClient(); |
72 | | - return useCallback( |
73 | | - async ( |
74 | | - name: string, |
75 | | - ixs: readonly Instruction[], |
76 | | - options: SendTXOptions = {}, |
77 | | - ): Promise<Signature> => { |
78 | | - const txId = Math.random().toString(36).substring(2, 15); |
79 | | - const baseEvent = { |
80 | | - id: txId, |
81 | | - title: name, |
82 | | - }; |
83 | | - if (!signer) { |
84 | | - internal_onTransactionStatusEvent({ |
85 | | - ...baseEvent, |
86 | | - type: "error-wallet-not-connected", |
87 | | - }); |
88 | | - throw new Error("Wallet not connected"); |
89 | | - } |
90 | | - |
91 | | - internal_onTransactionStatusEvent({ |
92 | | - ...baseEvent, |
93 | | - type: "preparing", |
94 | | - }); |
95 | | - |
96 | | - const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); |
97 | | - const transactionMessage = createTransaction({ |
98 | | - version: 0, |
99 | | - feePayer: signer, |
100 | | - instructions: [...ixs], |
101 | | - latestBlockhash, |
102 | | - // the compute budget values are HIGHLY recommend to be set in order to maximize your transaction landing rate |
103 | | - // TODO(igm): make this configurable and/or dynamic based on the instructions |
104 | | - computeUnitLimit: 1_400_000, |
105 | | - computeUnitPrice: 100_000n, |
106 | | - }); |
107 | | - |
108 | | - // Apply address lookup tables if provided to compress the transaction |
109 | | - const addressLookupTables = options.luts ?? {}; |
110 | | - const finalTransactionMessage = |
111 | | - Object.keys(addressLookupTables).length > 0 |
112 | | - ? compressTransactionMessageUsingAddressLookupTables( |
113 | | - transactionMessage, |
114 | | - addressLookupTables, |
115 | | - ) |
116 | | - : transactionMessage; |
117 | | - |
118 | | - internal_onTransactionStatusEvent({ |
119 | | - ...baseEvent, |
120 | | - type: "awaiting-wallet-signature", |
121 | | - }); |
122 | | - |
123 | | - // Send transaction using wallet adapter |
124 | | - const sigBytes = await signAndSendTransactionMessageWithSigners( |
125 | | - finalTransactionMessage, |
126 | | - ); |
127 | | - const decoder = getBase58Decoder(); |
128 | | - const sig = decoder.decode(sigBytes) as Signature; |
129 | | - const sentTxEvent = { |
130 | | - ...baseEvent, |
131 | | - sig, |
132 | | - explorerLink: getExplorerLink({ transaction: sig }), |
133 | | - }; |
134 | | - |
135 | | - internal_onTransactionStatusEvent({ |
136 | | - ...sentTxEvent, |
137 | | - type: "waiting-for-confirmation", |
138 | | - }); |
139 | | - |
140 | | - try { |
141 | | - // Wait for confirmation using modern RPC |
142 | | - const confirmationStrategy = { |
143 | | - signature: sig, |
144 | | - blockhash: latestBlockhash.blockhash, |
145 | | - lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, |
146 | | - }; |
147 | | - |
148 | | - // Poll for transaction confirmation |
149 | | - let confirmed = false; |
150 | | - let confirmationError: Error | null = null; |
151 | | - const maxRetries = 30; |
152 | | - let retries = 0; |
153 | | - |
154 | | - while (retries < maxRetries) { |
155 | | - try { |
156 | | - const signatureStatus = await rpc |
157 | | - .getSignatureStatuses([sig]) |
158 | | - .send(); |
159 | | - |
160 | | - if (signatureStatus.value[0]) { |
161 | | - const status = signatureStatus.value[0]; |
162 | | - if ( |
163 | | - status.confirmationStatus === "confirmed" || |
164 | | - status.confirmationStatus === "finalized" |
165 | | - ) { |
166 | | - confirmed = true; |
167 | | - if (status.err) { |
168 | | - confirmationError = new Error("Transaction failed on-chain"); |
169 | | - } |
170 | | - break; |
171 | | - } |
172 | | - } |
173 | | - |
174 | | - // Check if blockhash is still valid |
175 | | - const blockHeight = await rpc.getBlockHeight().send(); |
176 | | - if (blockHeight > confirmationStrategy.lastValidBlockHeight) { |
177 | | - throw new Error( |
178 | | - "Transaction expired - blockhash no longer valid", |
179 | | - ); |
180 | | - } |
181 | | - |
182 | | - // Wait before next attempt |
183 | | - await new Promise((resolve) => setTimeout(resolve, 1000)); |
184 | | - retries++; |
185 | | - } catch (error) { |
186 | | - console.error("Error checking transaction status:", error); |
187 | | - throw error; |
188 | | - } |
189 | | - } |
190 | | - |
191 | | - if (!confirmed) { |
192 | | - throw new Error("Transaction confirmation timeout"); |
193 | | - } |
194 | | - |
195 | | - if (confirmationError) { |
196 | | - throw confirmationError; |
197 | | - } |
198 | | - |
199 | | - // Get transaction details for logging using modern RPC |
200 | | - const result = await rpc |
201 | | - .getTransaction(sig, { |
202 | | - commitment: "confirmed", |
203 | | - maxSupportedTransactionVersion: 0, |
204 | | - encoding: "jsonParsed", |
205 | | - }) |
206 | | - .send(); |
207 | | - |
208 | | - if (result) { |
209 | | - // Reload the accounts that were written to |
210 | | - const writableAccounts = result.transaction.message.accountKeys |
211 | | - .filter((key) => key.writable) |
212 | | - .map((k) => k.pubkey); |
213 | | - await reloadAccounts(writableAccounts); |
214 | | - } |
215 | | - |
216 | | - internal_onTransactionStatusEvent({ |
217 | | - ...sentTxEvent, |
218 | | - type: "confirmed", |
219 | | - }); |
220 | | - |
221 | | - if (result?.meta?.logMessages) { |
222 | | - console.log(name, result.meta.logMessages.join("\n")); |
223 | | - } |
224 | | - |
225 | | - // Return the signature as a base58 string |
226 | | - return sig; |
227 | | - } catch (error: unknown) { |
228 | | - // Log error details for debugging |
229 | | - console.error(`${name} transaction failed:`, error); |
230 | | - |
231 | | - // Extract error logs |
232 | | - const extractErrorLogs = (err: unknown): string[] => { |
233 | | - if ( |
234 | | - err && |
235 | | - typeof err === "object" && |
236 | | - "logs" in err && |
237 | | - Array.isArray((err as { logs: unknown }).logs) |
238 | | - ) { |
239 | | - return (err as { logs: string[] }).logs; |
240 | | - } |
241 | | - if ( |
242 | | - err && |
243 | | - typeof err === "object" && |
244 | | - "context" in err && |
245 | | - typeof (err as { context: unknown }).context === "object" && |
246 | | - (err as { context: { logs?: unknown } }).context.logs && |
247 | | - Array.isArray((err as { context: { logs: unknown } }).context.logs) |
248 | | - ) { |
249 | | - return (err as { context: { logs: string[] } }).context.logs; |
250 | | - } |
251 | | - return []; |
252 | | - }; |
253 | | - |
254 | | - const errorLogs = extractErrorLogs(error); |
255 | | - if (errorLogs.length > 0) { |
256 | | - console.log("Transaction logs:"); |
257 | | - for (const log of errorLogs) { |
258 | | - console.log(" ", log); |
259 | | - } |
260 | | - } |
261 | | - |
262 | | - const errorMessage = |
263 | | - error instanceof Error ? error.message : "Transaction failed."; |
264 | | - |
265 | | - internal_onTransactionStatusEvent({ |
266 | | - ...sentTxEvent, |
267 | | - type: "error-transaction-failed", |
268 | | - errorMessage, |
269 | | - }); |
270 | | - throw error; |
271 | | - } |
272 | | - }, |
273 | | - [internal_onTransactionStatusEvent, reloadAccounts, rpc, signer], |
274 | | - ); |
| 12 | +export const useSendTX = () => { |
| 13 | + const { sendTX } = useGrillContext(); |
| 14 | + return sendTX; |
275 | 15 | }; |
0 commit comments