11use std:: str:: FromStr ;
22
3- use jsonrpc_core:: { Error , Result } ;
3+ use jsonrpc_core:: { BoxFuture , Error , Result } ;
44use jsonrpc_derive:: rpc;
55use sha2:: { Digest , Sha256 } ;
66use solana_client:: { rpc_config:: RpcSendTransactionConfig , rpc_custom_error:: RpcCustomError } ;
7+ use solana_rpc_client_api:: response:: Response as RpcResponse ;
78use solana_signature:: Signature ;
9+ use solana_transaction_status:: TransactionStatus ;
810
911use super :: {
1012 RunloopContext ,
@@ -59,6 +61,47 @@ pub trait Jito {
5961 transactions : Vec < String > ,
6062 config : Option < RpcSendTransactionConfig > ,
6163 ) -> Result < String > ;
64+
65+ /// Retrieves the statuses of all transactions in a previously submitted bundle.
66+ ///
67+ /// This RPC method looks up a bundle by its `bundle_id` (the SHA-256 hash returned by
68+ /// [`sendBundle`](#method.send_bundle)) and returns the signature statuses for the bundle's
69+ /// transactions in the same order they were recorded.
70+ ///
71+ /// ## Parameters
72+ /// - `bundle_id`: The bundle identifier returned by `sendBundle`.
73+ ///
74+ /// ## Returns
75+ /// A contextualized response containing:
76+ /// - `value`: A list of optional transaction statuses corresponding to the bundle signatures.
77+ /// Each entry can be:
78+ /// - `null` if the signature is unknown or not sufficiently confirmed for status reporting
79+ /// - a `TransactionStatus` object if the transaction is found and its status can be returned
80+ ///
81+ /// ## Example Request (JSON-RPC)
82+ /// ```json
83+ /// {
84+ /// "jsonrpc": "2.0",
85+ /// "id": 1,
86+ /// "method": "getBundleStatuses",
87+ /// "params": [
88+ /// "bundleIdHere"
89+ /// ]
90+ /// }
91+ /// ```
92+ ///
93+ /// ## Notes
94+ /// - Bundles are stored locally as a mapping from `bundle_id` to a list of base-58 signatures.
95+ /// - If the bundle ID is not known locally, an error is returned.
96+ /// - Status resolution is delegated to the same logic used by `getSignatureStatuses`:
97+ /// statuses are computed from locally stored transactions (and may fall back to a remote
98+ /// datasource, if configured).
99+ #[ rpc( meta, name = "getBundleStatuses" ) ]
100+ fn get_bundle_statuses (
101+ & self ,
102+ meta : Self :: Metadata ,
103+ bundle_id : String ,
104+ ) -> BoxFuture < Result < RpcResponse < Vec < Option < TransactionStatus > > > > > ;
62105}
63106
64107#[ derive( Clone ) ]
@@ -83,7 +126,7 @@ impl Jito for SurfpoolJitoRpc {
83126 ) ) ) ;
84127 }
85128
86- let Some ( _ctx ) = & meta else {
129+ let Some ( ctx ) = & meta else {
87130 return Err ( RpcCustomError :: NodeUnhealthy {
88131 num_slots_behind : None ,
89132 }
@@ -102,7 +145,7 @@ impl Jito for SurfpoolJitoRpc {
102145 let bundle_config = Some ( SurfpoolRpcSendTransactionConfig {
103146 base : RpcSendTransactionConfig {
104147 skip_preflight : true ,
105- ..base_config. clone ( )
148+ ..base_config
106149 } ,
107150 skip_sig_verify : None ,
108151 } ) ;
@@ -136,7 +179,40 @@ impl Jito for SurfpoolJitoRpc {
136179 let mut hasher = Sha256 :: new ( ) ;
137180 hasher. update ( concatenated_signatures. as_bytes ( ) ) ;
138181 let bundle_id = hasher. finalize ( ) ;
139- Ok ( hex:: encode ( bundle_id) )
182+ let bundle_id = hex:: encode ( bundle_id) ;
183+
184+ let _ = ctx
185+ . simnet_commands_tx
186+ . send ( surfpool_types:: SimnetCommand :: SendBundle ( (
187+ bundle_id. clone ( ) ,
188+ bundle_signatures
189+ . iter ( )
190+ . map ( |sig| sig. to_string ( ) )
191+ . collect ( ) ,
192+ ) ) ) ;
193+
194+ Ok ( bundle_id)
195+ }
196+
197+ fn get_bundle_statuses (
198+ & self ,
199+ meta : Self :: Metadata ,
200+ bundle_id : String ,
201+ ) -> BoxFuture < Result < RpcResponse < Vec < Option < TransactionStatus > > > > > {
202+ Box :: pin ( async move {
203+ let Some ( ctx) = & meta else {
204+ return Err ( RpcCustomError :: NodeUnhealthy {
205+ num_slots_behind : None ,
206+ }
207+ . into ( ) ) ;
208+ } ;
209+
210+ let signatures = ctx. svm_locker . get_bundle ( bundle_id) ?;
211+
212+ SurfpoolFullRpc
213+ . get_signature_statuses ( meta. clone ( ) , signatures, None )
214+ . await
215+ } )
140216 }
141217}
142218
@@ -421,4 +497,130 @@ mod tests {
421497 "Bundle ID should match SHA-256 of comma-separated signatures"
422498 ) ;
423499 }
500+
501+ #[ tokio:: test( flavor = "multi_thread" ) ]
502+ async fn test_send_bundle_persists_bundle_signatures ( ) {
503+ let payer = Keypair :: new ( ) ;
504+ let recipient = Pubkey :: new_unique ( ) ;
505+ let ( mempool_tx, mempool_rx) = crossbeam_channel:: unbounded ( ) ;
506+ let setup = TestSetup :: new_with_mempool ( SurfpoolJitoRpc , mempool_tx) ;
507+
508+ let recent_blockhash = setup
509+ . context
510+ . svm_locker
511+ . with_svm_reader ( |svm_reader| svm_reader. latest_blockhash ( ) ) ;
512+
513+ // Airdrop to payer so tx can succeed in our manual processing
514+ let _ = setup
515+ . context
516+ . svm_locker
517+ . 0
518+ . write ( )
519+ . await
520+ . airdrop ( & payer. pubkey ( ) , 2 * LAMPORTS_PER_SOL ) ;
521+
522+ let tx = build_v0_transaction (
523+ & payer. pubkey ( ) ,
524+ & [ & payer] ,
525+ & [ system_instruction:: transfer (
526+ & payer. pubkey ( ) ,
527+ & recipient,
528+ LAMPORTS_PER_SOL ,
529+ ) ] ,
530+ & recent_blockhash,
531+ ) ;
532+ let tx_encoded = bs58:: encode ( bincode:: serialize ( & tx) . unwrap ( ) ) . into_string ( ) ;
533+
534+ // Build expected signatures locally (what we expect to be persisted under bundle_id)
535+ let expected_sigs = vec ! [ tx. signatures[ 0 ] . to_string( ) ] ;
536+
537+ let setup_clone = setup. clone ( ) ;
538+ let handle = hiro_system_kit:: thread_named ( "send_bundle" )
539+ . spawn ( move || {
540+ setup_clone
541+ . rpc
542+ . send_bundle ( Some ( setup_clone. context ) , vec ! [ tx_encoded] , None )
543+ } )
544+ . unwrap ( ) ;
545+
546+ let mut processed_tx = false ;
547+ let mut processed_bundle = false ;
548+ let mut bundle_id_from_cmd: Option < String > = None ;
549+ let mut sigs_from_cmd: Option < Vec < String > > = None ;
550+
551+ while !( processed_tx && processed_bundle) {
552+ match mempool_rx. recv ( ) {
553+ Ok ( SimnetCommand :: ProcessTransaction ( _, tx, status_tx, _, _) ) => {
554+ let mut writer = setup. context . svm_locker . 0 . write ( ) . await ;
555+ let slot = writer. get_latest_absolute_slot ( ) ;
556+ writer. transactions_queued_for_confirmation . push_back ( (
557+ tx. clone ( ) ,
558+ status_tx. clone ( ) ,
559+ None ,
560+ ) ) ;
561+ let sig = tx. signatures [ 0 ] ;
562+ let tx_with_status_meta = TransactionWithStatusMeta {
563+ slot,
564+ transaction : tx,
565+ ..Default :: default ( )
566+ } ;
567+ writer
568+ . transactions
569+ . store (
570+ sig. to_string ( ) ,
571+ SurfnetTransactionStatus :: processed (
572+ tx_with_status_meta,
573+ std:: collections:: HashSet :: new ( ) ,
574+ ) ,
575+ )
576+ . unwrap ( ) ;
577+ status_tx
578+ . send ( TransactionStatusEvent :: Success (
579+ TransactionConfirmationStatus :: Confirmed ,
580+ ) )
581+ . unwrap ( ) ;
582+ processed_tx = true ;
583+ }
584+ Ok ( SimnetCommand :: SendBundle ( ( bundle_id, signatures) ) ) => {
585+ setup
586+ . context
587+ . svm_locker
588+ . process_bundle ( bundle_id. clone ( ) , signatures. clone ( ) )
589+ . unwrap ( ) ;
590+ bundle_id_from_cmd = Some ( bundle_id) ;
591+ sigs_from_cmd = Some ( signatures) ;
592+ processed_bundle = true ;
593+ }
594+ Ok ( SimnetCommand :: AirdropProcessed ) => continue ,
595+ other => panic ! ( "unexpected simnet command: {:?}" , other) ,
596+ }
597+ }
598+
599+ let result = handle. join ( ) . unwrap ( ) . expect ( "sendBundle should succeed" ) ;
600+ let stored_bundle_id = bundle_id_from_cmd. expect ( "should have received SendBundle command" ) ;
601+ assert_eq ! (
602+ result, stored_bundle_id,
603+ "sendBundle result bundle id should match stored bundle id"
604+ ) ;
605+
606+ let persisted = setup
607+ . context
608+ . svm_locker
609+ . get_bundle ( stored_bundle_id. clone ( ) )
610+ . expect ( "bundle should be persisted" ) ;
611+ assert ! (
612+ !persisted. is_empty( ) ,
613+ "svm_locker.get_bundle(bundle_id) should not be empty"
614+ ) ;
615+
616+ let sigs_from_cmd = sigs_from_cmd. expect ( "should have captured signatures from SendBundle" ) ;
617+ assert_eq ! (
618+ sigs_from_cmd, expected_sigs,
619+ "Signatures in SendBundle command should match locally built signatures"
620+ ) ;
621+ assert_eq ! (
622+ persisted, expected_sigs,
623+ "Persisted bundle signatures should match locally built signatures"
624+ ) ;
625+ }
424626}
0 commit comments