1
- import { AnchorProvider , Wallet } from "@coral-xyz/anchor" ;
1
+ import { Wallet } from "@coral-xyz/anchor" ;
2
2
import {
3
3
ComputeBudgetProgram ,
4
- ConfirmOptions ,
5
4
Connection ,
6
5
PACKET_DATA_SIZE ,
7
6
PublicKey ,
@@ -11,6 +10,7 @@ import {
11
10
TransactionMessage ,
12
11
VersionedTransaction ,
13
12
} from "@solana/web3.js" ;
13
+ import bs58 from "bs58" ;
14
14
15
15
/**
16
16
* If the transaction doesn't contain a `setComputeUnitLimit` instruction, the default compute budget is 200,000 units per instruction.
@@ -40,6 +40,7 @@ export type InstructionWithEphemeralSigners = {
40
40
export type PriorityFeeConfig = {
41
41
/** This is the priority fee in micro lamports, it gets passed down to `setComputeUnitPrice` */
42
42
computeUnitPriceMicroLamports ?: number ;
43
+ tightComputeBudget ?: boolean ;
43
44
} ;
44
45
45
46
/**
@@ -186,14 +187,19 @@ export class TransactionBuilder {
186
187
async buildVersionedTransactions (
187
188
args : PriorityFeeConfig
188
189
) : Promise < { tx : VersionedTransaction ; signers : Signer [ ] } [ ] > {
189
- const blockhash = ( await this . connection . getLatestBlockhash ( ) ) . blockhash ;
190
+ const blockhash = (
191
+ await this . connection . getLatestBlockhash ( { commitment : "confirmed" } )
192
+ ) . blockhash ;
190
193
191
194
return this . transactionInstructions . map (
192
195
( { instructions, signers, computeUnits } ) => {
193
196
const instructionsWithComputeBudget : TransactionInstruction [ ] = [
194
197
...instructions ,
195
198
] ;
196
- if ( computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions . length ) {
199
+ if (
200
+ computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions . length ||
201
+ args . tightComputeBudget
202
+ ) {
197
203
instructionsWithComputeBudget . push (
198
204
ComputeBudgetProgram . setComputeUnitLimit ( { units : computeUnits } )
199
205
) ;
@@ -226,21 +232,33 @@ export class TransactionBuilder {
226
232
buildLegacyTransactions (
227
233
args : PriorityFeeConfig
228
234
) : { tx : Transaction ; signers : Signer [ ] } [ ] {
229
- return this . transactionInstructions . map ( ( { instructions, signers } ) => {
230
- const instructionsWithComputeBudget = args . computeUnitPriceMicroLamports
231
- ? [
232
- ...instructions ,
235
+ return this . transactionInstructions . map (
236
+ ( { instructions, signers, computeUnits } ) => {
237
+ const instructionsWithComputeBudget : TransactionInstruction [ ] = [
238
+ ...instructions ,
239
+ ] ;
240
+ if (
241
+ computeUnits > DEFAULT_COMPUTE_BUDGET_UNITS * instructions . length ||
242
+ args . tightComputeBudget
243
+ ) {
244
+ instructionsWithComputeBudget . push (
245
+ ComputeBudgetProgram . setComputeUnitLimit ( { units : computeUnits } )
246
+ ) ;
247
+ }
248
+ if ( args . computeUnitPriceMicroLamports ) {
249
+ instructionsWithComputeBudget . push (
233
250
ComputeBudgetProgram . setComputeUnitPrice ( {
234
251
microLamports : args . computeUnitPriceMicroLamports ,
235
- } ) ,
236
- ]
237
- : instructions ;
252
+ } )
253
+ ) ;
254
+ }
238
255
239
- return {
240
- tx : new Transaction ( ) . add ( ...instructionsWithComputeBudget ) ,
241
- signers : signers ,
242
- } ;
243
- } ) ;
256
+ return {
257
+ tx : new Transaction ( ) . add ( ...instructionsWithComputeBudget ) ,
258
+ signers : signers ,
259
+ } ;
260
+ }
261
+ ) ;
244
262
}
245
263
246
264
/**
@@ -295,8 +313,16 @@ export class TransactionBuilder {
295
313
}
296
314
}
297
315
316
+ export const isVersionedTransaction = (
317
+ tx : Transaction | VersionedTransaction
318
+ ) : tx is VersionedTransaction => {
319
+ return "version" in tx ;
320
+ } ;
321
+
322
+ const TX_RETRY_INTERVAL = 500 ;
323
+
298
324
/**
299
- * Send a set of transactions to the network
325
+ * Send a set of transactions to the network based on https://github.com/rpcpool/optimized-txs-examples
300
326
*/
301
327
export async function sendTransactions (
302
328
transactions : {
@@ -305,12 +331,97 @@ export async function sendTransactions(
305
331
} [ ] ,
306
332
connection : Connection ,
307
333
wallet : Wallet ,
308
- opts ?: ConfirmOptions
334
+ maxRetries ?: number
309
335
) {
310
- if ( opts === undefined ) {
311
- opts = AnchorProvider . defaultOptions ( ) ;
312
- }
336
+ const blockhashResult = await connection . getLatestBlockhashAndContext ( {
337
+ commitment : "confirmed" ,
338
+ } ) ;
339
+
340
+ // Signing logic for versioned transactions is different from legacy transactions
341
+ for ( const transaction of transactions ) {
342
+ const { signers } = transaction ;
343
+ let tx = transaction . tx ;
344
+ if ( isVersionedTransaction ( tx ) ) {
345
+ if ( signers ) {
346
+ tx . sign ( signers ) ;
347
+ }
348
+ } else {
349
+ tx . feePayer = tx . feePayer ?? wallet . publicKey ;
350
+ tx . recentBlockhash = blockhashResult . value . blockhash ;
351
+
352
+ if ( signers ) {
353
+ for ( const signer of signers ) {
354
+ tx . partialSign ( signer ) ;
355
+ }
356
+ }
357
+ }
358
+
359
+ tx = await wallet . signTransaction ( tx ) ;
360
+
361
+ // In the following section, we wait and constantly check for the transaction to be confirmed
362
+ // and resend the transaction if it is not confirmed within a certain time interval
363
+ // thus handling tx retries on the client side rather than relying on the RPC
364
+ let confirmedTx = null ;
365
+ let retryCount = 0 ;
366
+
367
+ try {
368
+ // Get the signature of the transaction with different logic for versioned transactions
369
+ const txSignature = bs58 . encode (
370
+ isVersionedTransaction ( tx )
371
+ ? tx . signatures ?. [ 0 ] || new Uint8Array ( )
372
+ : tx . signature ?? new Uint8Array ( )
373
+ ) ;
313
374
314
- const provider = new AnchorProvider ( connection , wallet , opts ) ;
315
- await provider . sendAll ( transactions ) ;
375
+ const confirmTransactionPromise = connection . confirmTransaction (
376
+ {
377
+ signature : txSignature ,
378
+ blockhash : blockhashResult . value . blockhash ,
379
+ lastValidBlockHeight : blockhashResult . value . lastValidBlockHeight ,
380
+ } ,
381
+ "confirmed"
382
+ ) ;
383
+
384
+ confirmedTx = null ;
385
+ while ( ! confirmedTx ) {
386
+ confirmedTx = await Promise . race ( [
387
+ confirmTransactionPromise ,
388
+ new Promise ( ( resolve ) =>
389
+ setTimeout ( ( ) => {
390
+ resolve ( null ) ;
391
+ } , TX_RETRY_INTERVAL )
392
+ ) ,
393
+ ] ) ;
394
+ if ( confirmedTx ) {
395
+ break ;
396
+ }
397
+ if ( maxRetries && maxRetries < retryCount ) {
398
+ break ;
399
+ }
400
+ console . log (
401
+ "Retrying transaction: " ,
402
+ txSignature ,
403
+ " Retry count: " ,
404
+ retryCount
405
+ ) ;
406
+ retryCount ++ ;
407
+
408
+ await connection . sendRawTransaction ( tx . serialize ( ) , {
409
+ // Skipping preflight i.e. tx simulation by RPC as we simulated the tx above
410
+ // This allows Triton RPCs to send the transaction through multiple pathways for the fastest delivery
411
+ skipPreflight : true ,
412
+ // Setting max retries to 0 as we are handling retries manually
413
+ // Set this manually so that the default is skipped
414
+ maxRetries : 0 ,
415
+ preflightCommitment : "confirmed" ,
416
+ minContextSlot : blockhashResult . context . slot ,
417
+ } ) ;
418
+ }
419
+ } catch ( error ) {
420
+ console . error ( error ) ;
421
+ }
422
+
423
+ if ( ! confirmedTx ) {
424
+ throw new Error ( "Failed to land the transaction" ) ;
425
+ }
426
+ }
316
427
}
0 commit comments