55//! migration from Sui JSON RPC to gRPC by gradually migrating callsites away from the JSON RPC
66//! Client [`SuiClient`].
77
8- use std:: time:: Duration ;
8+ use std:: { str :: FromStr , time:: Duration } ;
99
1010use anyhow:: Context ;
1111use bytes:: Bytes ;
@@ -18,8 +18,10 @@ use sui_rpc::{
1818 BatchGetObjectsResponse ,
1919 Bcs ,
2020 DynamicField ,
21+ ExecutedTransaction ,
2122 GetBalanceRequest ,
2223 GetObjectRequest ,
24+ GetTransactionRequest ,
2325 ListDynamicFieldsRequest ,
2426 ListOwnedObjectsRequest ,
2527 ListOwnedObjectsResponse ,
@@ -28,11 +30,26 @@ use sui_rpc::{
2830 state_service_client:: StateServiceClient ,
2931 } ,
3032} ;
31- use sui_sdk:: { SuiClient , SuiClientBuilder } ;
33+ use sui_sdk:: {
34+ SuiClient ,
35+ SuiClientBuilder ,
36+ rpc_types:: {
37+ BalanceChange as SuiBalanceChange ,
38+ BcsEvent ,
39+ SuiEvent ,
40+ SuiTransactionBlockResponse ,
41+ SuiTransactionBlockResponseOptions ,
42+ } ,
43+ } ;
3244use sui_types:: {
3345 TypeTag ,
3446 base_types:: { ObjectID , ObjectRef , SuiAddress } ,
47+ crypto:: ToFromBytes ,
3548 digests:: TransactionDigest ,
49+ event:: EventID ,
50+ object:: Owner ,
51+ signature:: GenericSignature ,
52+ transaction:: { SenderSignedData , TransactionData } ,
3653} ;
3754use tonic:: service:: interceptor:: InterceptedService ;
3855use walrus_core:: ensure;
@@ -160,6 +177,70 @@ impl DualClient {
160177 . context ( "parsing previous_transaction" ) ?)
161178 }
162179
180+ /// Get a transaction response from the Sui network via gRPC.
181+ ///
182+ /// Handles: checkpoint, timestamp, raw_input, balance_changes, and events.
183+ /// Falls back to JSON-RPC for show_input, show_effects, show_object_changes,
184+ /// show_raw_effects.
185+ pub async fn get_transaction_response_grpc (
186+ & self ,
187+ digest : TransactionDigest ,
188+ options : & SuiTransactionBlockResponseOptions ,
189+ ) -> Result < SuiTransactionBlockResponse , SuiClientError > {
190+ let field_mask = build_transaction_field_mask ( options) ;
191+ let executed_tx = self . get_transaction_raw ( digest, field_mask) . await ?;
192+ executed_transaction_to_response ( digest, options, & executed_tx)
193+ }
194+
195+ /// Get events for a transaction from the Sui network via gRPC.
196+ pub async fn get_transaction_events_grpc (
197+ & self ,
198+ tx_digest : TransactionDigest ,
199+ ) -> Result < Vec < SuiEvent > , SuiClientError > {
200+ let field_mask = FieldMask :: from_paths ( & [ ExecutedTransaction :: path_builder ( )
201+ . events ( )
202+ . events ( )
203+ . finish ( ) ] ) ;
204+ let executed_tx = self . get_transaction_raw ( tx_digest, field_mask) . await ?;
205+ let empty = Vec :: new ( ) ;
206+ executed_tx
207+ . events
208+ . as_ref ( )
209+ . map ( |e| & e. events )
210+ . unwrap_or ( & empty)
211+ . iter ( )
212+ . enumerate ( )
213+ . map ( |( idx, event) | grpc_event_to_sui_event ( tx_digest, idx, event) )
214+ . collect :: < Result < Vec < _ > , _ > > ( )
215+ }
216+
217+ /// Low-level gRPC call to get a transaction with a specific field mask.
218+ ///
219+ /// Uses `Box::pin` to erase the future type and avoid `Send` recursion overflow
220+ /// from the deep `ExecutedTransaction` proto type tree.
221+ fn get_transaction_raw (
222+ & self ,
223+ digest : TransactionDigest ,
224+ field_mask : FieldMask ,
225+ ) -> std:: pin:: Pin <
226+ Box <
227+ dyn std:: future:: Future < Output = Result < ExecutedTransaction , SuiClientError > >
228+ + Send
229+ + ' _ ,
230+ > ,
231+ > {
232+ let mut grpc_client: GrpcClient = self . grpc_client . clone ( ) ;
233+ Box :: pin ( async move {
234+ let sui_digest = sui_sdk_types:: Digest :: new ( digest. into_inner ( ) ) ;
235+ let request = GetTransactionRequest :: new ( & sui_digest) . with_read_mask ( field_mask) ;
236+ let response = grpc_client. ledger_client ( ) . get_transaction ( request) . await ?;
237+ Ok ( response
238+ . into_inner ( )
239+ . transaction
240+ . context ( "no transaction in get_transaction_response" ) ?)
241+ } )
242+ }
243+
163244 /// Get the BCS representation of an object's contents and its type from the Sui network.
164245 pub async fn get_object_contents (
165246 & self ,
@@ -657,6 +738,220 @@ fn convert_grpc_object_to_coin(object: Object) -> Result<Coin, anyhow::Error> {
657738 } )
658739}
659740
741+ /// Build a [`FieldMask`] for `GetTransactionRequest` based on the requested options.
742+ ///
743+ /// Always requests `digest`, `checkpoint`, and `timestamp`. Conditionally adds paths for
744+ /// `raw_input` (transaction BCS + signature BCS), `balance_changes`, and `events`.
745+ fn build_transaction_field_mask ( options : & SuiTransactionBlockResponseOptions ) -> FieldMask {
746+ let mut paths: Vec < String > = vec ! [
747+ ExecutedTransaction :: path_builder( ) . digest( ) ,
748+ ExecutedTransaction :: path_builder( ) . checkpoint( ) ,
749+ ExecutedTransaction :: path_builder( ) . timestamp( ) ,
750+ ] ;
751+ if options. show_raw_input {
752+ paths. push (
753+ ExecutedTransaction :: path_builder ( )
754+ . transaction ( )
755+ . bcs ( )
756+ . finish ( ) ,
757+ ) ;
758+ paths. push (
759+ ExecutedTransaction :: path_builder ( )
760+ . signatures ( )
761+ . bcs ( )
762+ . finish ( ) ,
763+ ) ;
764+ }
765+ if options. show_balance_changes {
766+ paths. push (
767+ ExecutedTransaction :: path_builder ( )
768+ . balance_changes ( )
769+ . finish ( ) ,
770+ ) ;
771+ }
772+ if options. show_events {
773+ paths. push (
774+ ExecutedTransaction :: path_builder ( )
775+ . events ( )
776+ . events ( )
777+ . finish ( ) ,
778+ ) ;
779+ }
780+ FieldMask :: from_paths ( & paths)
781+ }
782+
783+ /// Convert a gRPC [`ExecutedTransaction`] into a [`SuiTransactionBlockResponse`].
784+ fn executed_transaction_to_response (
785+ digest : TransactionDigest ,
786+ options : & SuiTransactionBlockResponseOptions ,
787+ executed_tx : & ExecutedTransaction ,
788+ ) -> Result < SuiTransactionBlockResponse , SuiClientError > {
789+ let mut response = SuiTransactionBlockResponse :: new ( digest) ;
790+
791+ response. checkpoint = executed_tx. checkpoint ;
792+ response. timestamp_ms = executed_tx
793+ . timestamp
794+ . as_ref ( )
795+ . map ( |ts| {
796+ u64:: try_from ( ts. seconds )
797+ . context ( "negative timestamp" )
798+ . map ( |s| s * 1000 + u64:: try_from ( ts. nanos ) . unwrap_or ( 0 ) / 1_000_000 )
799+ } )
800+ . transpose ( ) ?;
801+
802+ if options. show_raw_input {
803+ response. raw_transaction = reconstruct_raw_transaction ( executed_tx) ?;
804+ }
805+
806+ if options. show_balance_changes {
807+ response. balance_changes = Some (
808+ executed_tx
809+ . balance_changes
810+ . iter ( )
811+ . map ( convert_grpc_balance_change)
812+ . collect :: < Result < Vec < _ > , _ > > ( ) ?,
813+ ) ;
814+ }
815+
816+ if options. show_events
817+ && let Some ( events) = & executed_tx. events
818+ {
819+ let sui_events: Vec < SuiEvent > = events
820+ . events
821+ . iter ( )
822+ . enumerate ( )
823+ . map ( |( idx, event) | grpc_event_to_sui_event ( digest, idx, event) )
824+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
825+ response. events = Some ( sui_sdk:: rpc_types:: SuiTransactionBlockEvents { data : sui_events } ) ;
826+ }
827+
828+ Ok ( response)
829+ }
830+
831+ /// Reconstruct the BCS-encoded `SenderSignedData` (raw_transaction) from gRPC fields.
832+ fn reconstruct_raw_transaction (
833+ executed_tx : & ExecutedTransaction ,
834+ ) -> Result < Vec < u8 > , SuiClientError > {
835+ let tx_bcs = executed_tx
836+ . transaction
837+ . as_ref ( )
838+ . context ( "no transaction in executed_transaction" ) ?
839+ . bcs
840+ . as_ref ( )
841+ . context ( "no bcs in transaction" ) ?;
842+
843+ let tx_data: TransactionData = bcs:: from_bytes (
844+ tx_bcs
845+ . value
846+ . as_ref ( )
847+ . context ( "no value in transaction bcs" ) ?,
848+ )
849+ . context ( "deserializing TransactionData from gRPC bcs" ) ?;
850+
851+ let tx_signatures: Vec < GenericSignature > = executed_tx
852+ . signatures
853+ . iter ( )
854+ . map ( |sig| {
855+ let sig_bcs = sig. bcs . as_ref ( ) . context ( "no bcs in user signature" ) ?;
856+ let sig_bytes = sig_bcs
857+ . value
858+ . as_ref ( )
859+ . context ( "no value in signature bcs" ) ?;
860+ GenericSignature :: from_bytes ( sig_bytes)
861+ . or_else ( |_| {
862+ bcs:: from_bytes :: < GenericSignature > ( sig_bytes) . map_err ( |e| {
863+ fastcrypto:: error:: FastCryptoError :: GeneralError ( e. to_string ( ) )
864+ } )
865+ } )
866+ . context ( "parsing GenericSignature from gRPC bcs" )
867+ } )
868+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
869+
870+ let sender_signed_data = SenderSignedData :: new ( tx_data, tx_signatures) ;
871+ Ok ( bcs:: to_bytes ( & sender_signed_data) . context ( "serializing SenderSignedData" ) ?)
872+ }
873+
874+ /// Convert a gRPC `BalanceChange` to the SDK `BalanceChange`.
875+ fn convert_grpc_balance_change (
876+ change : & sui_rpc:: proto:: sui:: rpc:: v2:: BalanceChange ,
877+ ) -> Result < SuiBalanceChange , SuiClientError > {
878+ let address: SuiAddress = change
879+ . address
880+ . as_ref ( )
881+ . context ( "no address in balance change" ) ?
882+ . parse ( )
883+ . context ( "parsing balance change address" ) ?;
884+ let coin_type: TypeTag = change
885+ . coin_type
886+ . as_ref ( )
887+ . context ( "no coin_type in balance change" ) ?
888+ . parse ( )
889+ . context ( "parsing balance change coin_type" ) ?;
890+ let amount: i128 = change
891+ . amount
892+ . as_ref ( )
893+ . context ( "no amount in balance change" ) ?
894+ . parse ( )
895+ . context ( "parsing balance change amount" ) ?;
896+ Ok ( SuiBalanceChange {
897+ owner : Owner :: AddressOwner ( address) ,
898+ coin_type,
899+ amount,
900+ } )
901+ }
902+
903+ /// Convert a gRPC `Event` to a `SuiEvent`.
904+ fn grpc_event_to_sui_event (
905+ tx_digest : TransactionDigest ,
906+ event_seq : usize ,
907+ event : & sui_rpc:: proto:: sui:: rpc:: v2:: Event ,
908+ ) -> Result < SuiEvent , SuiClientError > {
909+ let package_id: ObjectID = event
910+ . package_id
911+ . as_ref ( )
912+ . context ( "no package_id in event" ) ?
913+ . parse ( )
914+ . context ( "parsing event package_id" ) ?;
915+ let transaction_module = move_core_types:: identifier:: Identifier :: from_str (
916+ event. module . as_ref ( ) . context ( "no module in event" ) ?,
917+ )
918+ . context ( "parsing event module" ) ?;
919+ let sender: SuiAddress = event
920+ . sender
921+ . as_ref ( )
922+ . context ( "no sender in event" ) ?
923+ . parse ( )
924+ . context ( "parsing event sender" ) ?;
925+ let type_: StructTag = event
926+ . event_type
927+ . as_ref ( )
928+ . context ( "no event_type in event" ) ?
929+ . parse ( )
930+ . context ( "parsing event type" ) ?;
931+ let contents = event
932+ . contents
933+ . as_ref ( )
934+ . context ( "no contents in event" ) ?
935+ . value
936+ . as_ref ( )
937+ . context ( "no value in event contents" ) ?
938+ . to_vec ( ) ;
939+
940+ Ok ( SuiEvent {
941+ id : EventID {
942+ tx_digest,
943+ event_seq : u64:: try_from ( event_seq) . context ( "event_seq overflow" ) ?,
944+ } ,
945+ package_id,
946+ transaction_module,
947+ sender,
948+ type_,
949+ parsed_json : serde_json:: Value :: Null ,
950+ bcs : BcsEvent :: new ( contents) ,
951+ timestamp_ms : None ,
952+ } )
953+ }
954+
660955async fn batch_get_objects < T > (
661956 mut grpc_client : GrpcClient ,
662957 object_ids : & [ ObjectID ] ,
0 commit comments