@@ -13,6 +13,7 @@ import { isNullLike, waitMS } from '@tensor-hq/ts-utils';
1313import bs58 from 'bs58' ;
1414import { backOff } from 'exponential-backoff' ;
1515import { getLatestBlockHeight } from './rpc' ;
16+ import { VersionedTransactionResponse } from '@solana/web3.js' ;
1617
1718const BLOCK_TIME_MS = 400 ;
1819
@@ -58,6 +59,7 @@ export class RetryTxSender {
5859 private start ?: number ;
5960 private txSig ?: TransactionSignature ;
6061 private confirmedTx ?: ConfirmedTx ;
62+ private fetchedTx ?: VersionedTransactionResponse ;
6163 readonly connection : Connection ;
6264 readonly additionalConnections : Connection [ ] ;
6365 readonly logger ?: Logger ;
@@ -93,6 +95,12 @@ export class RetryTxSender {
9395 this . retrySleep = retrySleep ;
9496 }
9597
98+ /**
99+ * Send transaction to RPCs and asynchronously retry sending until
100+ * 1. The transaction is confirmed via tryConfirm/tryFetchTx
101+ * 2. The transaction times out
102+ * 3. Confirmation is cancelled via cancelConfirm
103+ */
96104 async send (
97105 tx : Transaction | VersionedTransaction ,
98106 ) : Promise < TransactionSignature > {
@@ -149,6 +157,26 @@ export class RetryTxSender {
149157 return this . txSig ;
150158 }
151159
160+ /**
161+ * Confirm the status of a transaction sent by this sender by
162+ * 1. Polling getSignatureStatus
163+ * 2. Optionally listening for the onSignature WS event
164+ *
165+ * Stops polling once
166+ * 1. The transaction is confirmed
167+ * 2. The transaction times out (via timeout promise or lastValidBlockHeight)
168+ * 3. Confirmation is cancelled via cancelConfirm
169+ *
170+ * Notes:
171+ * * After confirming, subsequent calls will return a cached ConfirmedTx
172+ * * tryConfirm should not be invoked multiple times in parallel
173+ * * tryConfirm should not be invoked in parallel with tryFetchTx
174+ *
175+ * @param lastValidBlockHeight cancel tx confirmation loop once this block height is reached
176+ * @param opts {
177+ * @param disableWs don't listen for onSignature WS events when confirming
178+ * }
179+ */
152180 async tryConfirm (
153181 lastValidBlockHeight ?: number ,
154182 opts ?: ConfirmOpts ,
@@ -162,6 +190,7 @@ export class RetryTxSender {
162190 throw new Error ( 'you need to send the tx first' ) ;
163191 }
164192
193+ this . done = false ;
165194 try {
166195 const result = await this . _confirmTransaction (
167196 this . txSig ,
@@ -182,6 +211,56 @@ export class RetryTxSender {
182211 }
183212 }
184213
214+ /**
215+ * Fetch a transaction sent by this sender by polling getTransaction.
216+ *
217+ * Stops polling once
218+ * 1. The transaction is fetched
219+ * 2. The transaction times out (via timeout promise or lastValidBlockHeight)
220+ * 3. Confirmation is cancelled via cancelConfirm
221+ *
222+ * Notes:
223+ * * After confirming, subsequent calls will return a cached tx
224+ * * tryFetchTx should not be invoked multiple times in parallel
225+ * * tryFetchTx should not be invoked in parallel with tryConfirm
226+ *
227+ * @param lastValidBlockHeight cancel tx confirmation loop once this block height is reached
228+ * @param opts {
229+ * @param disableWs don't listen for onSignature WS events when confirming
230+ * }
231+ */
232+ async tryFetchTx (
233+ lastValidBlockHeight ?: number ,
234+ ) : Promise < VersionedTransactionResponse > {
235+ if ( this . fetchedTx ) {
236+ this . logger ?. info ( '✅ Tx already fetched' ) ;
237+ return this . fetchedTx ;
238+ }
239+
240+ if ( ! this . txSig ) {
241+ throw new Error ( 'you need to send the tx first' ) ;
242+ }
243+
244+ this . done = false ;
245+ try {
246+ this . fetchedTx = await this . _fetchTransaction (
247+ this . txSig ,
248+ lastValidBlockHeight ,
249+ ) ;
250+ this . confirmedTx = {
251+ txSig : this . txSig ,
252+ slot : this . fetchedTx . slot ,
253+ err : this . fetchedTx . meta ?. err ?? null ,
254+ } ;
255+ return this . fetchedTx ;
256+ } catch ( e ) {
257+ this . logger ?. error ( `${ JSON . stringify ( e ) } ` ) ;
258+ throw e ;
259+ } finally {
260+ this . _stopWaiting ( ) ;
261+ }
262+ }
263+
185264 cancelConfirm ( ) {
186265 if ( this . cancelReference . resolve ) {
187266 this . cancelReference . resolve ( ) ;
@@ -204,10 +283,11 @@ export class RetryTxSender {
204283 throw new Error ( 'signature must be base58 encoded: ' + txSig ) ;
205284 }
206285
207- if ( decodedSignature . length !== 64 )
286+ if ( decodedSignature . length !== 64 ) {
208287 throw new Error (
209288 `signature has invalid length ${ decodedSignature . length } (expected 64)` ,
210289 ) ;
290+ }
211291
212292 this . start = Date . now ( ) ;
213293 const subscriptionCommitment = this . opts . commitment ;
@@ -223,7 +303,9 @@ export class RetryTxSender {
223303
224304 const pollPromise = backOff (
225305 async ( ) => {
226- this . logger ?. debug ( '[getSignatureStatus] Attept to get sig status' ) ;
306+ this . logger ?. debug (
307+ '[getSignatureStatus] Attempt to get sig status' ,
308+ ) ;
227309 const { value, context } = await connection . getSignatureStatus (
228310 txSig ,
229311 {
@@ -350,6 +432,105 @@ export class RetryTxSender {
350432 return response ;
351433 }
352434
435+ private async _fetchTransaction (
436+ txSig : TransactionSignature ,
437+ lastValidBlockHeight ?: number ,
438+ ) : Promise < VersionedTransactionResponse > {
439+ this . logger ?. info ( `⏳ [${ txSig . substring ( 0 , 5 ) } ] begin trying to fetch tx` ) ;
440+
441+ let decodedSignature : Uint8Array ;
442+ try {
443+ decodedSignature = bs58 . decode ( txSig ) ;
444+ } catch ( err ) {
445+ throw new Error ( 'signature must be base58 encoded: ' + txSig ) ;
446+ }
447+
448+ if ( decodedSignature . length !== 64 ) {
449+ throw new Error (
450+ `signature has invalid length ${ decodedSignature . length } (expected 64)` ,
451+ ) ;
452+ }
453+
454+ this . start = Date . now ( ) ;
455+ const connections = [ this . connection , ...this . additionalConnections ] ;
456+ let response : VersionedTransactionResponse | null = null ;
457+
458+ const promises = connections . map ( ( connection ) =>
459+ backOff (
460+ async ( ) => {
461+ this . logger ?. debug ( '[getTransaction] Attempt to get sig status' ) ;
462+ const maybeTx = await connection . getTransaction ( txSig , {
463+ commitment : 'confirmed' ,
464+ maxSupportedTransactionVersion : 0 ,
465+ } ) ;
466+ if ( ! maybeTx ) {
467+ this . logger ?. debug (
468+ `[getTransaction] tx ${ txSig } not found, try again in ${ this . retrySleep } ms` ,
469+ ) ;
470+ throw new Error ( `tx ${ txSig } not found` ) ;
471+ }
472+ return maybeTx ;
473+ } ,
474+ {
475+ maxDelay : this . retrySleep ,
476+ startingDelay : this . retrySleep ,
477+ numOfAttempts : Math . ceil ( this . timeout / this . retrySleep ) ,
478+ retry : ( e ) => {
479+ if (
480+ typeof e . message === 'string' &&
481+ e . message . endsWith ( 'not found' )
482+ ) {
483+ this . logger ?. info ( `sig ${ txSig } not found yet, retrying` ) ;
484+ } else {
485+ console . error ( `[getTransaction] received error, ${ e } retrying` ) ;
486+ }
487+ return ! this . done ;
488+ } ,
489+ } ,
490+ )
491+ . then ( ( res ) => {
492+ response = res ;
493+ } )
494+ . catch ( ( err ) => {
495+ this . logger ?. error (
496+ `[${ txSig . substring ( 0 , 5 ) } ] error polling: ${ err } ` ,
497+ ) ;
498+ } ) ,
499+ ) ;
500+
501+ await this . _racePromises (
502+ txSig ,
503+ promises ,
504+ this . timeout ,
505+ lastValidBlockHeight ,
506+ ) ;
507+
508+ const duration = ( Date . now ( ) - this . start ) / 1000 ;
509+ if ( response === null ) {
510+ const errMsg = `❌ [${ txSig . substring (
511+ 0 ,
512+ 5 ,
513+ ) } ] NOT confirmed in ${ duration . toFixed ( 2 ) } sec`;
514+ this . logger ?. error ( errMsg ) ;
515+ throw new Error ( errMsg ) ;
516+ }
517+
518+ if ( ( < VersionedTransactionResponse > response ) . meta ?. err ) {
519+ this . logger ?. warn (
520+ `⚠️ [${ txSig . substring (
521+ 0 ,
522+ 5 ,
523+ ) } ] confirmed AS FAILED TX in ${ duration . toFixed ( 2 ) } sec`,
524+ ) ;
525+ } else {
526+ this . logger ?. info (
527+ `✅ [${ txSig . substring ( 0 , 5 ) } ] confirmed in ${ duration . toFixed ( 2 ) } sec` ,
528+ ) ;
529+ }
530+
531+ return response ;
532+ }
533+
353534 private _getTimestamp ( ) : number {
354535 return new Date ( ) . getTime ( ) ;
355536 }
0 commit comments