@@ -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 ,
@@ -32,7 +34,11 @@ use sui_sdk::{SuiClient, SuiClientBuilder};
3234use sui_types:: {
3335 TypeTag ,
3436 base_types:: { ObjectID , ObjectRef , SuiAddress } ,
37+ crypto:: ToFromBytes ,
3538 digests:: TransactionDigest ,
39+ event:: EventID ,
40+ signature:: GenericSignature ,
41+ transaction:: { SenderSignedData , TransactionData } ,
3642} ;
3743use tonic:: service:: interceptor:: InterceptedService ;
3844use walrus_core:: ensure;
@@ -42,6 +48,7 @@ use crate::{
4248 coin:: Coin ,
4349 contracts:: { AssociatedContractStruct , TypeOriginMap } ,
4450 dynamic_field_info:: { DynamicFieldPage , dynamic_field_info_from_grpc} ,
51+ types:: { BalanceChange , EventEnvelope , TransactionResponse , TransactionResponseOptions } ,
4552} ;
4653
4754/// The maximum number of objects to request in a single "batch" gRPC call.
@@ -160,6 +167,47 @@ impl DualClient {
160167 . context ( "parsing previous_transaction" ) ?)
161168 }
162169
170+ /// Get a transaction response from the Sui network via gRPC.
171+ ///
172+ /// Supports requesting raw_input, balance_changes, and events via the options parameter.
173+ /// Always includes digest, checkpoint, and timestamp.
174+ pub async fn get_transaction_response_grpc (
175+ & self ,
176+ digest : TransactionDigest ,
177+ options : & TransactionResponseOptions ,
178+ ) -> Result < TransactionResponse , SuiClientError > {
179+ let field_mask = build_transaction_field_mask ( options) ;
180+ let executed_tx = self . get_transaction_raw ( digest, field_mask) . await ?;
181+ executed_transaction_to_response ( digest, options, & executed_tx)
182+ }
183+
184+ /// Low-level gRPC call to get a transaction with a specific field mask.
185+ ///
186+ /// Uses `Box::pin` to erase the future type and avoid `Send` recursion overflow
187+ /// from the deep `ExecutedTransaction` proto type tree.
188+ fn get_transaction_raw (
189+ & self ,
190+ digest : TransactionDigest ,
191+ field_mask : FieldMask ,
192+ ) -> std:: pin:: Pin <
193+ Box <
194+ dyn std:: future:: Future < Output = Result < ExecutedTransaction , SuiClientError > >
195+ + Send
196+ + ' _ ,
197+ > ,
198+ > {
199+ let mut grpc_client: GrpcClient = self . grpc_client . clone ( ) ;
200+ Box :: pin ( async move {
201+ let sui_digest = sui_sdk_types:: Digest :: new ( digest. into_inner ( ) ) ;
202+ let request = GetTransactionRequest :: new ( & sui_digest) . with_read_mask ( field_mask) ;
203+ let response = grpc_client. ledger_client ( ) . get_transaction ( request) . await ?;
204+ Ok ( response
205+ . into_inner ( )
206+ . transaction
207+ . context ( "no transaction in get_transaction_response" ) ?)
208+ } )
209+ }
210+
163211 /// Get the BCS representation of an object's contents and its type from the Sui network.
164212 pub async fn get_object_contents (
165213 & self ,
@@ -657,6 +705,210 @@ fn convert_grpc_object_to_coin(object: Object) -> Result<Coin, anyhow::Error> {
657705 } )
658706}
659707
708+ /// Build a [`FieldMask`] for `GetTransactionRequest` based on the requested options.
709+ ///
710+ /// Always requests `digest`, `checkpoint`, and `timestamp`. Conditionally adds paths for
711+ /// `raw_input` (transaction BCS + signature BCS), `balance_changes`, and `events`.
712+ fn build_transaction_field_mask ( options : & TransactionResponseOptions ) -> FieldMask {
713+ let mut paths: Vec < String > = vec ! [
714+ ExecutedTransaction :: path_builder( ) . digest( ) ,
715+ ExecutedTransaction :: path_builder( ) . checkpoint( ) ,
716+ ExecutedTransaction :: path_builder( ) . timestamp( ) ,
717+ ] ;
718+ if options. show_raw_input {
719+ paths. push (
720+ ExecutedTransaction :: path_builder ( )
721+ . transaction ( )
722+ . bcs ( )
723+ . finish ( ) ,
724+ ) ;
725+ paths. push (
726+ ExecutedTransaction :: path_builder ( )
727+ . signatures ( )
728+ . bcs ( )
729+ . finish ( ) ,
730+ ) ;
731+ }
732+ if options. show_balance_changes {
733+ paths. push (
734+ ExecutedTransaction :: path_builder ( )
735+ . balance_changes ( )
736+ . finish ( ) ,
737+ ) ;
738+ }
739+ if options. show_events {
740+ paths. push (
741+ ExecutedTransaction :: path_builder ( )
742+ . events ( )
743+ . events ( )
744+ . finish ( ) ,
745+ ) ;
746+ }
747+ FieldMask :: from_paths ( & paths)
748+ }
749+
750+ /// Convert a gRPC [`ExecutedTransaction`] into a [`TransactionResponse`].
751+ fn executed_transaction_to_response (
752+ digest : TransactionDigest ,
753+ options : & TransactionResponseOptions ,
754+ executed_tx : & ExecutedTransaction ,
755+ ) -> Result < TransactionResponse , SuiClientError > {
756+ let checkpoint = executed_tx. checkpoint ;
757+ let timestamp_ms = executed_tx
758+ . timestamp
759+ . as_ref ( )
760+ . map ( |ts| {
761+ let seconds = u64:: try_from ( ts. seconds ) . context ( "negative timestamp seconds" ) ?;
762+ let nanos = u64:: try_from ( ts. nanos ) . context ( "negative timestamp nanos" ) ?;
763+ anyhow:: Ok ( seconds * 1000 + nanos / 1_000_000 )
764+ } )
765+ . transpose ( ) ?;
766+
767+ let raw_transaction = if options. show_raw_input {
768+ reconstruct_raw_transaction ( executed_tx) ?
769+ } else {
770+ Vec :: new ( )
771+ } ;
772+
773+ let balance_changes = if options. show_balance_changes {
774+ Some (
775+ executed_tx
776+ . balance_changes
777+ . iter ( )
778+ . map ( convert_grpc_balance_change)
779+ . collect :: < Result < Vec < _ > , _ > > ( ) ?,
780+ )
781+ } else {
782+ None
783+ } ;
784+
785+ let events = if options. show_events {
786+ executed_tx
787+ . events
788+ . as_ref ( )
789+ . map ( |events| {
790+ events
791+ . events
792+ . iter ( )
793+ // Event index matches on-chain event_seq; protobuf repeated fields
794+ // preserve order and the gRPC server serializes from the same vector.
795+ . enumerate ( )
796+ . map ( |( idx, event) | grpc_event_to_event_envelope ( digest, idx, event) )
797+ . collect :: < Result < Vec < _ > , _ > > ( )
798+ } )
799+ . transpose ( ) ?
800+ } else {
801+ None
802+ } ;
803+
804+ Ok ( TransactionResponse {
805+ digest,
806+ checkpoint,
807+ timestamp_ms,
808+ raw_transaction,
809+ balance_changes,
810+ events,
811+ } )
812+ }
813+
814+ /// Reconstruct the BCS-encoded `SenderSignedData` (raw_transaction) from gRPC fields.
815+ fn reconstruct_raw_transaction (
816+ executed_tx : & ExecutedTransaction ,
817+ ) -> Result < Vec < u8 > , SuiClientError > {
818+ let tx_bcs = executed_tx
819+ . transaction
820+ . as_ref ( )
821+ . context ( "no transaction in executed_transaction" ) ?
822+ . bcs
823+ . as_ref ( )
824+ . context ( "no bcs in transaction" ) ?;
825+
826+ let tx_data: TransactionData = bcs:: from_bytes (
827+ tx_bcs
828+ . value
829+ . as_ref ( )
830+ . context ( "no value in transaction bcs" ) ?,
831+ )
832+ . context ( "deserializing TransactionData from gRPC bcs" ) ?;
833+
834+ let tx_signatures: Vec < GenericSignature > = executed_tx
835+ . signatures
836+ . iter ( )
837+ . map ( |sig| {
838+ let sig_bcs = sig. bcs . as_ref ( ) . context ( "no bcs in user signature" ) ?;
839+ let sig_bytes = sig_bcs
840+ . value
841+ . as_ref ( )
842+ . context ( "no value in signature bcs" ) ?;
843+ GenericSignature :: from_bytes ( sig_bytes)
844+ . context ( "parsing GenericSignature from gRPC bcs" )
845+ } )
846+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
847+
848+ let sender_signed_data = SenderSignedData :: new ( tx_data, tx_signatures) ;
849+ Ok ( bcs:: to_bytes ( & sender_signed_data) . context ( "serializing SenderSignedData" ) ?)
850+ }
851+
852+ /// Convert a gRPC `BalanceChange` to our protocol-agnostic [`BalanceChange`].
853+ fn convert_grpc_balance_change (
854+ change : & sui_rpc:: proto:: sui:: rpc:: v2:: BalanceChange ,
855+ ) -> Result < BalanceChange , SuiClientError > {
856+ let address: SuiAddress = change
857+ . address
858+ . as_ref ( )
859+ . context ( "no address in balance change" ) ?
860+ . parse ( )
861+ . context ( "parsing balance change address" ) ?;
862+ let coin_type: TypeTag = change
863+ . coin_type
864+ . as_ref ( )
865+ . context ( "no coin_type in balance change" ) ?
866+ . parse ( )
867+ . context ( "parsing balance change coin_type" ) ?;
868+ let amount: i128 = change
869+ . amount
870+ . as_ref ( )
871+ . context ( "no amount in balance change" ) ?
872+ . parse ( )
873+ . context ( "parsing balance change amount" ) ?;
874+ Ok ( BalanceChange {
875+ address,
876+ coin_type,
877+ amount,
878+ } )
879+ }
880+
881+ /// Convert a gRPC `Event` to an [`EventEnvelope`].
882+ fn grpc_event_to_event_envelope (
883+ tx_digest : TransactionDigest ,
884+ event_seq : usize ,
885+ event : & sui_rpc:: proto:: sui:: rpc:: v2:: Event ,
886+ ) -> Result < EventEnvelope , SuiClientError > {
887+ let type_: StructTag = event
888+ . event_type
889+ . as_ref ( )
890+ . context ( "no event_type in event" ) ?
891+ . parse ( )
892+ . context ( "parsing event type" ) ?;
893+ let bcs = event
894+ . contents
895+ . as_ref ( )
896+ . context ( "no contents in event" ) ?
897+ . value
898+ . as_ref ( )
899+ . context ( "no value in event contents" ) ?
900+ . to_vec ( ) ;
901+
902+ Ok ( EventEnvelope {
903+ id : EventID {
904+ tx_digest,
905+ event_seq : u64:: try_from ( event_seq) . context ( "event_seq overflow" ) ?,
906+ } ,
907+ type_,
908+ bcs,
909+ } )
910+ }
911+
660912async fn batch_get_objects < T > (
661913 mut grpc_client : GrpcClient ,
662914 object_ids : & [ ObjectID ] ,
0 commit comments