Skip to content

Commit 7f6038d

Browse files
committed
feat(sui): add gRPC migration level 5 for transaction reads
1 parent d8641b5 commit 7f6038d

File tree

2 files changed

+382
-3
lines changed

2 files changed

+382
-3
lines changed

crates/walrus-sui/src/client/dual_client.rs

Lines changed: 297 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
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

1010
use anyhow::Context;
1111
use 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+
};
3244
use 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
};
3754
use tonic::service::interceptor::InterceptedService;
3855
use 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+
660955
async fn batch_get_objects<T>(
661956
mut grpc_client: GrpcClient,
662957
object_ids: &[ObjectID],

0 commit comments

Comments
 (0)