1
- import { AddressLookupTableAccount , Commitment , ComputeBudgetProgram , Connection , PublicKey , TransactionInstruction , TransactionMessage , VersionedTransaction } from "@solana/web3.js" ;
1
+ import {
2
+ AddressLookupTableAccount ,
3
+ Commitment ,
4
+ ComputeBudgetProgram ,
5
+ Connection ,
6
+ Keypair ,
7
+ PublicKey ,
8
+ SignatureStatus ,
9
+ Transaction ,
10
+ TransactionInstruction ,
11
+ TransactionMessage ,
12
+ VersionedTransaction ,
13
+ } from "@solana/web3.js" ;
2
14
import { getErrorFromRPCResponse } from "./logs" ;
3
15
4
16
export const confirmTransaction = async (
@@ -52,6 +64,265 @@ export const getSimulationComputeUnits = async (
52
64
sigVerify : false ,
53
65
} ) ;
54
66
55
- getErrorFromRPCResponse ( rpcResponse ) ;
67
+ if ( rpcResponse ?. value ?. err ) {
68
+ const logs = rpcResponse . value . logs ?. join ( "\n • " ) || "No logs available" ;
69
+ throw new Error ( `Transaction simulation failed:\n •${ logs } ` ) ;
70
+ }
71
+
56
72
return rpcResponse . value . unitsConsumed || null ;
57
- } ;
73
+ } ;
74
+
75
+ /**
76
+ * Constants for transaction retry configuration
77
+ */
78
+ export const RETRY_INTERVAL_MS = 2000 ;
79
+ export const MAX_RETRIES = 30 ;
80
+
81
+ /**
82
+ * Represents the different states of a transaction during its lifecycle
83
+ * @property status - The current status of the transaction
84
+ * @property signature - The transaction signature (only present when status is "sent")
85
+ * @property result - The signature status (only present when status is "confirmed")
86
+ */
87
+ export type TxStatusUpdate =
88
+ | { status : "created" }
89
+ | { status : "signed" }
90
+ | { status : "sent" ; signature : string }
91
+ | { status : "confirmed" ; result : SignatureStatus } ;
92
+
93
+ /**
94
+ * Configuration options for transaction retry mechanism
95
+ * @property maxRetries - Maximum number of retry attempts
96
+ * @property initialDelayMs - Delay between retries in milliseconds
97
+ * @property commitment - Desired commitment level for the transaction
98
+ * @property skipPreflight - Whether to skip transaction simulation
99
+ * @property onStatusUpdate - Callback function to receive transaction status updates
100
+ */
101
+ export type SendTransactionOptions = Partial < {
102
+ maxRetries : number ;
103
+ initialDelayMs : number ;
104
+ commitment : Commitment ;
105
+ onStatusUpdate : ( status : TxStatusUpdate ) => void ;
106
+ skipPreflight : boolean ;
107
+ } > ;
108
+
109
+ /**
110
+ * Configuration for compute unit buffer calculation
111
+ * @property multiplier - Multiply simulated units by this value (e.g., 1.1 adds 10%)
112
+ * @property fixed - Add this fixed amount of compute units
113
+ */
114
+ export type ComputeUnitBuffer = {
115
+ multiplier ?: number ;
116
+ fixed ?: number ;
117
+ } ;
118
+
119
+ /**
120
+ * Default configuration values for transaction sending
121
+ */
122
+ export const DEFAULT_SEND_OPTIONS : Required <
123
+ Omit < SendTransactionOptions , "onStatusUpdate" >
124
+ > = {
125
+ maxRetries : MAX_RETRIES ,
126
+ initialDelayMs : RETRY_INTERVAL_MS ,
127
+ commitment : "confirmed" ,
128
+ skipPreflight : true ,
129
+ } ;
130
+
131
+ /**
132
+ * Sends a transaction with automatic retries and status updates
133
+ *
134
+ * @param connection - The Solana connection object
135
+ * @param transaction - The transaction to send
136
+ * @param signers - Array of signers needed for the transaction
137
+ * @param options - Optional configuration for the retry mechanism
138
+ *
139
+ * @returns Promise that resolves to the transaction signature
140
+ *
141
+ * @remarks
142
+ * This function implements a robust retry mechanism that:
143
+ * 1. Signs the transaction (if signers are provided)
144
+ * 2. Sends the transaction only once
145
+ * 3. Monitors the transaction status until confirmation
146
+ * 4. Retries on failure with a fixed delay
147
+ * 5. Provides detailed status updates through the callback
148
+ *
149
+ * The function uses default values that can be partially overridden through the options parameter.
150
+ * Default values are defined in DEFAULT_SEND_OPTIONS.
151
+ *
152
+ * Status updates include:
153
+ * - "created": Initial transaction state
154
+ * - "signed": Transaction has been signed
155
+ * - "sent": Transaction has been sent (includes signature)
156
+ * - "confirmed": Transaction is confirmed or finalized
157
+ *
158
+ * @throws Error if the transaction fails after all retry attempts
159
+ *
160
+ * @example
161
+ * ```typescript
162
+ * const signature = await sendTransactionWithRetry(
163
+ * connection,
164
+ * transaction,
165
+ * signers,
166
+ * {
167
+ * onStatusUpdate: (status) => console.log(status),
168
+ * commitment: "confirmed"
169
+ * }
170
+ * );
171
+ * ```
172
+ */
173
+ export async function sendTransactionWithRetry (
174
+ connection : Connection ,
175
+ transaction : Transaction ,
176
+ signers : Keypair [ ] ,
177
+ {
178
+ maxRetries = DEFAULT_SEND_OPTIONS . maxRetries ,
179
+ initialDelayMs = DEFAULT_SEND_OPTIONS . initialDelayMs ,
180
+ commitment = DEFAULT_SEND_OPTIONS . commitment ,
181
+ skipPreflight = DEFAULT_SEND_OPTIONS . skipPreflight ,
182
+ onStatusUpdate = ( ) => { } ,
183
+ } : SendTransactionOptions = { } ,
184
+ ) : Promise < string > {
185
+ onStatusUpdate ?.( { status : "created" } ) ;
186
+
187
+ // Sign the transaction
188
+ if ( signers . length > 0 ) {
189
+ transaction . sign ( ...signers ) ;
190
+ }
191
+
192
+ onStatusUpdate ?.( { status : "signed" } ) ;
193
+
194
+ let signature : string | null = null ;
195
+ let status : SignatureStatus | null = null ;
196
+ let retries = 0 ;
197
+
198
+ while ( retries < maxRetries ) {
199
+ try {
200
+ // Send transaction if not sent yet
201
+ if ( ! signature ) {
202
+ signature = await connection . sendRawTransaction (
203
+ transaction . serialize ( ) ,
204
+ {
205
+ skipPreflight,
206
+ preflightCommitment : commitment ,
207
+ maxRetries : 0 ,
208
+ } ,
209
+ ) ;
210
+ onStatusUpdate ?.( { status : "sent" , signature } ) ;
211
+ }
212
+
213
+ // Check status
214
+ const response = await connection . getSignatureStatus ( signature ) ;
215
+ if ( response ?. value ) {
216
+ status = response . value ;
217
+
218
+ if (
219
+ status . confirmationStatus === "confirmed" ||
220
+ status . confirmationStatus === "finalized"
221
+ ) {
222
+ onStatusUpdate ?.( { status : "confirmed" , result : status } ) ;
223
+ return signature ;
224
+ }
225
+ }
226
+ } catch ( error : unknown ) {
227
+ if ( error instanceof Error ) {
228
+ console . log ( `Attempt ${ retries + 1 } failed:` , error . message ) ;
229
+ } else {
230
+ console . log ( `Attempt ${ retries + 1 } failed:` , error ) ;
231
+ }
232
+ }
233
+
234
+ retries ++ ;
235
+ if ( retries < maxRetries ) {
236
+ await new Promise ( ( resolve ) => setTimeout ( resolve , initialDelayMs ) ) ;
237
+ }
238
+ }
239
+
240
+ throw new Error ( `Transaction failed after ${ maxRetries } attempts` ) ;
241
+ }
242
+
243
+ /**
244
+ * Prepares a transaction by adding compute budget instructions
245
+ *
246
+ * @param connection - The Solana connection object
247
+ * @param tx - The transaction to prepare
248
+ * @param payer - The public key of the transaction payer
249
+ * @param priorityFee - Priority fee in microLamports (default: 1000)
250
+ * @param computeUnitBuffer - Optional buffer to add to simulated compute units
251
+ *
252
+ * @remarks
253
+ * This function:
254
+ * 1. Adds a compute unit price instruction with the specified priority fee
255
+ * 2. Simulates the transaction to determine required compute units
256
+ * 3. Applies any specified compute unit buffers
257
+ * 4. Adds a compute unit limit instruction based on the simulation
258
+ *
259
+ * The compute unit buffer can be specified as:
260
+ * - A multiplier (e.g., 1.1 adds 10% to simulated units)
261
+ * - A fixed value (e.g., 1000 adds 1000 compute units)
262
+ * - Both (multiplier is applied first, then fixed value is added)
263
+ *
264
+ * Priority Fees:
265
+ * To find an appropriate priority fee, refer to your RPC provider's documentation:
266
+ * - Helius: https://docs.helius.dev/solana-apis/priority-fee-api
267
+ * - Triton: https://docs.triton.one/chains/solana/improved-priority-fees-api
268
+ * - Quicknode: https://www.quicknode.com/docs/solana/qn_estimatePriorityFees
269
+ *
270
+ * @throws If the transaction simulation fails
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * // Add 10% buffer plus 1000 fixed compute units
275
+ * await prepareTransactionWithCompute(
276
+ * connection,
277
+ * transaction,
278
+ * payer.publicKey,
279
+ * 1000,
280
+ * { multiplier: 1.1, fixed: 1000 }
281
+ * );
282
+ * ```
283
+ */
284
+ export async function prepareTransactionWithCompute (
285
+ connection : Connection ,
286
+ tx : Transaction ,
287
+ payer : PublicKey ,
288
+ priorityFee : number = 1000 ,
289
+ computeUnitBuffer : ComputeUnitBuffer = { } ,
290
+ ) : Promise < void > {
291
+ tx . add (
292
+ ComputeBudgetProgram . setComputeUnitPrice ( {
293
+ microLamports : priorityFee ,
294
+ } ) ,
295
+ ) ;
296
+
297
+ const simulatedCompute = await getSimulationComputeUnits (
298
+ connection ,
299
+ tx . instructions ,
300
+ payer ,
301
+ [ ] ,
302
+ ) ;
303
+
304
+ if ( simulatedCompute === null ) {
305
+ throw new Error ( "Failed to simulate compute units" ) ;
306
+ }
307
+
308
+ console . log ( "Simulated compute units" , simulatedCompute ) ;
309
+
310
+ // Apply buffer to compute units
311
+ let finalComputeUnits = simulatedCompute ;
312
+ if ( computeUnitBuffer . multiplier ) {
313
+ finalComputeUnits = Math . floor (
314
+ finalComputeUnits * computeUnitBuffer . multiplier ,
315
+ ) ;
316
+ }
317
+ if ( computeUnitBuffer . fixed ) {
318
+ finalComputeUnits += computeUnitBuffer . fixed ;
319
+ }
320
+
321
+ console . log ( "Final compute units (with buffer)" , finalComputeUnits ) ;
322
+
323
+ tx . add (
324
+ ComputeBudgetProgram . setComputeUnitLimit ( {
325
+ units : finalComputeUnits ,
326
+ } ) ,
327
+ ) ;
328
+ }
0 commit comments