@@ -22,6 +22,8 @@ import {
22
22
ParsedAccountData ,
23
23
PublicKey ,
24
24
SendOptions ,
25
+ SendTransactionError ,
26
+ TransactionExpiredBlockheightExceededError ,
25
27
} from '@solana/web3.js' ;
26
28
import { SolanaAddress , SolanaZeroAddress } from './address' ;
27
29
import {
@@ -160,6 +162,52 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
160
162
return balancesArr . reduce ( ( obj , item ) => Object . assign ( obj , item ) , { } ) ;
161
163
}
162
164
165
+ // Handles retrying a Transaction if the error is deemed to be
166
+ // recoverable. Currently handles:
167
+ // - Blockhash not found (blockhash too new for the node we submitted to)
168
+ // - Not enough bytes (storage account not seen yet)
169
+
170
+ private static async sendWithRetry (
171
+ rpc : Connection ,
172
+ stxns : SignedTx ,
173
+ opts : SendOptions ,
174
+ retries : number = 3 ,
175
+ ) : Promise < string > {
176
+ // Shouldnt get hit but just in case
177
+ if ( ! retries ) throw new Error ( 'Too many retries' ) ;
178
+
179
+ try {
180
+ const txid = await rpc . sendRawTransaction ( stxns . tx , opts ) ;
181
+ return txid ;
182
+ } catch ( e ) {
183
+ retries -= 1 ;
184
+ if ( ! retries ) throw e ;
185
+
186
+ // Would require re-signing, for now bail
187
+ if ( e instanceof TransactionExpiredBlockheightExceededError ) throw e ;
188
+
189
+ // Only handle SendTransactionError
190
+ if ( ! ( e instanceof SendTransactionError ) ) throw e ;
191
+ const emsg = e . message ;
192
+
193
+ // Only handle simulation errors
194
+ if ( ! emsg . includes ( 'Transaction simulation failed' ) ) throw e ;
195
+
196
+ // Blockhash not found _yet_
197
+ if ( emsg . includes ( 'Blockhash not found' ) )
198
+ return this . sendWithRetry ( rpc , stxns , opts , retries ) ;
199
+
200
+ // Find the log message with the error details
201
+ const loggedErr = e . logs . find ( ( log ) =>
202
+ log . startsWith ( 'Program log: Error: ' ) ,
203
+ ) ;
204
+
205
+ // Probably caused by storage account not seen yet
206
+ if ( loggedErr && loggedErr . includes ( 'Not enough bytes' ) )
207
+ return this . sendWithRetry ( rpc , stxns , opts , retries ) ;
208
+ }
209
+ }
210
+
163
211
static async sendWait (
164
212
chain : Chain ,
165
213
rpc : Connection ,
@@ -168,14 +216,16 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
168
216
) : Promise < TxHash [ ] > {
169
217
const { blockhash, lastValidBlockHeight } = await this . latestBlock ( rpc ) ;
170
218
171
- // Set the commitment level to match the rpc commitment level
172
- // otherwise, it defaults to finalized
173
- if ( ! opts ) opts = { preflightCommitment : rpc . commitment } ;
174
-
175
219
const txhashes = await Promise . all (
176
- stxns . map ( ( stxn ) => {
177
- return rpc . sendRawTransaction ( stxn , opts ) ;
178
- } ) ,
220
+ stxns . map ( ( stxn ) =>
221
+ this . sendWithRetry (
222
+ rpc ,
223
+ stxn ,
224
+ // Set the commitment level to match the rpc commitment level
225
+ // otherwise, it defaults to finalized
226
+ opts ?? { preflightCommitment : rpc . commitment } ,
227
+ ) ,
228
+ ) ,
179
229
) ;
180
230
181
231
await Promise . all (
@@ -198,11 +248,13 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
198
248
rpc : Connection ,
199
249
commitment ?: Commitment ,
200
250
) : Promise < { blockhash : string ; lastValidBlockHeight : number } > {
251
+ // Use finalized to prevent blockhash not found errors
252
+ // Note: this may mean we have less time to submit transactions?
201
253
return rpc . getLatestBlockhash ( commitment ?? 'finalized' ) ;
202
254
}
203
255
204
256
static async getLatestBlock ( rpc : Connection ) : Promise < number > {
205
- const { lastValidBlockHeight } = await this . latestBlock ( rpc ) ;
257
+ const { lastValidBlockHeight } = await this . latestBlock ( rpc , 'confirmed' ) ;
206
258
return lastValidBlockHeight ;
207
259
}
208
260
0 commit comments