Skip to content

Commit 8633fc9

Browse files
authored
chore: gRPC migration level 5 — transaction reads (#3224)
Migrates `get_transaction_with_options` and `get_events` to gRPC at migration level 5, and decouples Walrus from some `sui_sdk` response types. ### gRPC transaction reads (level 5) Routes `get_transaction_with_options` through `LedgerService.GetTransaction` when `WALRUS_GRPC_MIGRATION_LEVEL >= 5`. The gRPC path handles call sites needing checkpoint, timestamp, raw_input, balance_changes, and/or events. Added conversion helpers for reconstructing `SenderSignedData` BCS, mapping gRPC `BalanceChange`/`Event` types, and building field masks. `query_transaction_blocks` stays JSON-RPC (no gRPC equivalent, only used in benchmarks). ### Protocol-agnostic types Introduced `EventEnvelope`, `BalanceChange`, `TransactionResponse`, and `TransactionResponseOptions` in `walrus-sui/src/types/` to replace some `sui_sdk::rpc_types` at the Walrus API boundary. Converted all 24 `TryFrom<SuiEvent>` impls to `TryFrom<EventEnvelope>`. JSON-RPC boundaries convert to the new types at the call site. ### Consolidated `get_events` `get_events` now routes through `get_transaction_with_options` for both gRPC and JSON-RPC paths, eliminating the separate `event_api().get_events()` dependency. ### Stack overflow fixes Boxed the futures for `execute_transaction` and `get_transaction_with_options` to prevent stack overflows in deep async call chains.
1 parent 712d87d commit 8633fc9

File tree

13 files changed

+739
-271
lines changed

13 files changed

+739
-271
lines changed

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ Configured in `.config/nextest.toml`:
181181
- **simtest**: `num-cpus` threads, 30m timeout
182182
- **performance-test**: single-threaded, 15m timeout
183183

184+
## gRPC protobuf definitions
185+
186+
Sui Rust SDK houses the .proto files for the Sui gRPC API. Often these are stored locally at
187+
../sui-rust-sdk/crates/sui-rpc/vendored/proto/sui/rpc/v2/. If not, they are available at
188+
https://github.com/MystenLabs/sui-rust-sdk/tree/master/crates/sui-rpc/vendored/proto/sui/rpc/v2.
189+
184190
## Documentation
185191

186192
Documentation files live under `docs/content/` and use MDX (Docusaurus) format. The site is built

crates/walrus-service/src/event/event_processor/bootstrap.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
//! Bootstrap module for getting the initial committee and checkpoint information.
55
use anyhow::{Context as _, Result};
6-
use sui_sdk::rpc_types::SuiTransactionBlockResponseOptions;
76
use sui_types::{
87
base_types::ObjectID,
98
committee::Committee,
@@ -26,7 +25,7 @@ pub async fn get_bootstrap_committee_and_checkpoint(
2625
system_pkg_id: ObjectID,
2726
) -> Result<(Committee, VerifiedCheckpoint)> {
2827
let txn_digest = sui_client.get_previous_transaction(system_pkg_id).await?;
29-
let txn_options = SuiTransactionBlockResponseOptions::new();
28+
let txn_options = walrus_sui::types::TransactionResponseOptions::new();
3029
let txn = sui_client
3130
.get_transaction_with_options(txn_digest, txn_options)
3231
.await?;

crates/walrus-service/src/event/event_processor/checkpoint.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ impl CheckpointProcessor {
181181
None,
182182
move_datatype_layout,
183183
)?;
184-
let contract_event: ContractEvent = sui_event.try_into()?;
184+
let contract_event: ContractEvent =
185+
walrus_sui::types::EventEnvelope::from(sui_event).try_into()?;
185186
let event_sequence_number = CheckpointEventPosition::new(
186187
*checkpoint.checkpoint_summary.sequence_number(),
187188
counter,

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

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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};
3234
use 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
};
3743
use tonic::service::interceptor::InterceptedService;
3844
use 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+
660912
async fn batch_get_objects<T>(
661913
mut grpc_client: GrpcClient,
662914
object_ids: &[ObjectID],

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ use sui_sdk::{
2020
apis::EventApi,
2121
rpc_types::{
2222
EventFilter,
23-
SuiEvent,
2423
SuiObjectData,
2524
SuiObjectDataFilter,
2625
SuiObjectDataOptions,
@@ -1509,7 +1508,7 @@ async fn poll_for_events<U>(
15091508
mut last_event: Option<EventID>,
15101509
) -> Result<()>
15111510
where
1512-
U: TryFrom<SuiEvent> + Send + Sync + Debug + 'static,
1511+
U: TryFrom<crate::types::EventEnvelope> + Send + Sync + Debug + 'static,
15131512
{
15141513
// The actual interval with which we poll, increases if there is an RPC error
15151514
let mut polling_interval = initial_polling_interval;
@@ -1536,9 +1535,10 @@ where
15361535
event_id = ?event.id,
15371536
event_type = ?event.type_
15381537
);
1538+
let envelope = crate::types::EventEnvelope::from(event);
15391539

15401540
let continue_or_exit = async move {
1541-
let event_obj = match event.try_into() {
1541+
let event_obj = match envelope.try_into() {
15421542
Ok(event_obj) => event_obj,
15431543
Err(_) => {
15441544
tracing::error!("could not convert event");

0 commit comments

Comments
 (0)