1- use std:: { mem, time:: Duration } ;
1+ use std:: { mem, ops :: ControlFlow , time:: Duration } ;
22
33use mbv_core:: traits:: { ActionError , ActionsCallbackScheduler } ;
44use program:: outbox:: PendingTransaction ;
55use solana_pubkey:: Pubkey ;
6+ use solana_rpc_client:: rpc_client:: SerializableTransaction ;
67use solana_signature:: Signature ;
78use solana_signer:: Signer ;
89use tokio:: time:: timeout;
9- use tracing:: info;
10+ use tracing:: { error , info, warn } ;
1011
1112use super :: {
1213 error:: TransactionStrategyExecutionError ,
1314 recovery:: TransactionRecovery ,
1415 utils:: {
15- ExecutionState , handle_actions_result, handle_cpi_limit_error, transaction_execution_loop,
16+ handle_actions_result, handle_cpi_limit_error, prepare_transaction, recovery_commit_nonces,
17+ resolve_pending_signature,
1618 } ,
1719} ;
1820use crate :: {
1921 intent_executor:: {
2022 ExecutionOutput , IntentExecutionReport , IntentExecutorCtx ,
2123 error:: { IntentExecutorError , IntentExecutorResult } ,
22- progress:: ExecutionProgress ,
24+ progress:: { ExecutionProgress , TransactionProgress } ,
25+ } ,
26+ outbox:: {
27+ OutboxClient , outbox_intent_bundles_reader:: OutboxIntentBundlesReader ,
28+ progress:: PendingExecution ,
2329 } ,
24- outbox:: { OutboxClient , progress:: PendingExecution } ,
2530 tasks:: {
2631 BaseTaskImpl ,
2732 task_info_fetcher:: TaskInfoFetcher ,
@@ -33,6 +38,8 @@ use crate::{
3338#[ cfg( test) ]
3439mod tests;
3540
41+ const TRANSACTION_LOOP_CEILING : u8 = 10 ;
42+
3643/// Runs the selected transaction and any remaining actions or undelegations.
3744/// A confirmed commit is never included in subsequent recovery attempts.
3845pub ( in crate :: intent_executor) struct TransactionExecutor < ' a , T , F , A , O > {
@@ -192,47 +199,149 @@ where
192199 let ctx = self . ctx ;
193200 let confirmed_commit = self . report . progress . confirmed_commit ;
194201 let has_follow_up = self . follow_up . is_some ( ) ;
195- let make_outbox_stage = |transaction| {
196- PendingExecution {
197- transaction ,
198- confirmed_commit ,
199- has_follow_up ,
200- }
201- . into_stage ( )
202+ let recovery = TransactionRecovery {
203+ authority : & ctx . authority ,
204+ intent_client : & ctx . intent_client ,
205+ intent_id : self . intent_id ,
206+ callback_scheduler : & ctx . actions_callback_executor ,
207+ task_info_fetcher : & ctx . task_info_fetcher ,
208+ committed_pubkeys : self . committed_pubkeys ,
202209 } ;
203- let state = ExecutionState {
204- current_attempt : & mut self . current_attempt ,
205- transaction_strategy : & mut self . current ,
206- pending_transaction : & mut self . pending_transaction ,
207- execution_report : self . report ,
208- commit_signature : confirmed_commit,
209- } ;
210- transaction_execution_loop (
211- & ctx. authority ,
212- & ctx. intent_client ,
213- ctx. outbox_client . as_ref ( ) ,
214- & ctx. transaction_preparator ,
215- TransactionRecovery {
216- authority : & ctx. authority ,
217- intent_client : & ctx. intent_client ,
218- intent_id : self . intent_id ,
219- callback_scheduler : & ctx. actions_callback_executor ,
220- task_info_fetcher : & ctx. task_info_fetcher ,
221- committed_pubkeys : self . committed_pubkeys ,
222- } ,
223- self . intent_id ,
224- make_outbox_stage,
225- |status| {
226- PendingExecution :: from_status ( status)
227- . filter ( |pending| {
228- pending. has_follow_up == has_follow_up
229- && pending. confirmed_commit . is_some ( ) == confirmed_commit. is_some ( )
230- } )
231- . map ( |pending| pending. transaction )
232- } ,
233- state,
234- )
235- . await
210+ let mut reconciled_outbox = false ;
211+ loop {
212+ if self . pending_transaction . is_none ( ) && !reconciled_outbox {
213+ reconciled_outbox = true ;
214+ self . pending_transaction = self . recover_pending_from_outbox ( ) . await ;
215+ }
216+
217+ if let Some ( ref pending) = self . pending_transaction {
218+ self . report . progress . transaction =
219+ TransactionProgress :: Submitted ( pending. signature ) ;
220+ if resolve_pending_signature ( & ctx. intent_client , pending) . await ? {
221+ self . report . progress . transaction =
222+ TransactionProgress :: Confirmed ( pending. signature ) ;
223+ break Ok ( Ok ( pending. signature ) ) ;
224+ }
225+ self . report . progress . transaction = TransactionProgress :: Rejected ( pending. signature ) ;
226+ self . pending_transaction = None ;
227+ }
228+
229+ // Resolve a previously sent transaction before skipping an empty follow-up.
230+ if let Some ( commit_signature) = confirmed_commit
231+ && self . current . optimized_tasks . is_empty ( )
232+ {
233+ return Ok ( Ok ( commit_signature) ) ;
234+ }
235+
236+ self . current_attempt += 1 ;
237+
238+ // Prepare message
239+ let prepared_transaction = prepare_transaction (
240+ & ctx. intent_client ,
241+ & ctx. authority ,
242+ & ctx. transaction_preparator ,
243+ & mut self . current ,
244+ )
245+ . await
246+ . map_err ( IntentExecutorError :: FailedPreparation ) ?;
247+
248+ // Record in outbox before sending
249+ let pending = PendingTransaction {
250+ signature : * prepared_transaction. get_signature ( ) ,
251+ blockhash : * prepared_transaction. get_recent_blockhash ( ) ,
252+ } ;
253+ self . report . progress . transaction = TransactionProgress :: Prepared ( pending. signature ) ;
254+ let recovery_commit_nonces = recovery_commit_nonces ( & self . current . optimized_tasks ) ;
255+ ctx. outbox_client
256+ . set_intent_execution_stage (
257+ self . intent_id ,
258+ PendingExecution {
259+ transaction : pending,
260+ confirmed_commit,
261+ has_follow_up,
262+ }
263+ . into_stage ( ) ,
264+ recovery_commit_nonces,
265+ )
266+ . await
267+ . map_err ( Into :: into) ?;
268+
269+ // Now record locally signature of tx we about to send
270+ // Precaution for timeout in between
271+ self . pending_transaction = Some ( pending) ;
272+ self . report . progress . transaction = TransactionProgress :: Submitted ( pending. signature ) ;
273+ let signature = & pending. signature ;
274+
275+ // Send signed tx
276+ let execution_result = ctx
277+ . intent_client
278+ . send_signed_tx_with_retries ( & prepared_transaction, & self . current . optimized_tasks )
279+ . await ;
280+
281+ // Result returned, cleanup pending signature
282+ self . pending_transaction = None ;
283+
284+ let execution_err = match execution_result {
285+ Ok ( ( ) ) => {
286+ self . report . progress . transaction = TransactionProgress :: Confirmed ( * signature) ;
287+ return Ok ( Ok ( * signature) ) ;
288+ }
289+ Err ( err) => err,
290+ } ;
291+ if execution_err. is_rejected ( ) {
292+ self . report . progress . transaction = TransactionProgress :: Rejected ( * signature) ;
293+ }
294+ let flow = recovery. recover ( & execution_err, & mut self . current , self . report ) . await ?;
295+ let cleanup = match flow {
296+ ControlFlow :: Continue ( value) => value,
297+ ControlFlow :: Break ( ( ) ) => return Ok ( Err ( execution_err) ) ,
298+ } ;
299+ ctx. intent_client . invalidate_cached_blockhash ( ) . await ;
300+ self . report . dispose ( cleanup) ;
301+
302+ if let Some ( commit_signature) = confirmed_commit
303+ && self . current . optimized_tasks . is_empty ( )
304+ {
305+ // Failed actions can leave no work after a confirmed commit.
306+ // Keep undelegation failures visible even when patching removes all tasks.
307+ if matches ! (
308+ execution_err,
309+ TransactionStrategyExecutionError :: ActionsError ( ..)
310+ ) {
311+ self . report . add_patched_error ( execution_err) ;
312+ return Ok ( Ok ( commit_signature) ) ;
313+ }
314+ return Ok ( Err ( execution_err) ) ;
315+ }
316+
317+ if self . current_attempt >= TRANSACTION_LOOP_CEILING {
318+ error ! ( "CRITICAL! Recursion ceiling reached" ) ;
319+ return Ok ( Err ( execution_err) ) ;
320+ } else {
321+ self . report . add_patched_error ( execution_err) ;
322+ }
323+ }
324+ }
325+
326+ async fn recover_pending_from_outbox ( & self ) -> Option < PendingTransaction > {
327+ let reader = self . ctx . outbox_client . outbox_reader ( ) ;
328+ match reader. fetch_outbox_intent ( self . intent_id ) . await {
329+ Ok ( Some ( bundle) ) => PendingExecution :: from_status ( bundle. status ( ) )
330+ . filter ( |pending| {
331+ pending. has_follow_up == self . follow_up . is_some ( )
332+ && pending. confirmed_commit . is_some ( )
333+ == self . report . progress . confirmed_commit . is_some ( )
334+ } )
335+ . map ( |pending| pending. transaction ) ,
336+ Ok ( None ) => None ,
337+ Err ( _) => {
338+ warn ! (
339+ intent_id = self . intent_id,
340+ "Failed to reconcile pending transaction from outbox"
341+ ) ;
342+ None
343+ }
344+ }
236345 }
237346
238347 fn execute_callbacks ( & mut self , signature : Option < Signature > , result : Result < ( ) , ActionError > ) {
0 commit comments