Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/iota-sdk-grpc-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ tonic = { workspace = true, features = ["zstd"] }

# internal dependencies
iota-grpc-types.workspace = true
iota-types = { workspace = true, features = ["serde", "hash"] }
iota-types = { workspace = true, features = ["serde", "hash", "rand"] }
Comment thread
Thoralf-M marked this conversation as resolved.
Outdated

[features]
default = ["tls-ring", "tls-native-roots"]
Expand Down
25 changes: 10 additions & 15 deletions crates/iota-sdk-grpc-client/src/api/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,18 @@ pub type Result<T> = std::result::Result<T, Error>;
// Field Masks
// =============================================================================

/// A read mask that can be passed to client methods.
/// A low-level read mask string.
///
/// Construct from field path constants defined in
/// [`read_mask_fields`](crate::read_mask_fields):
/// Most callers should use the scoped per-endpoint mask types in
/// [`read_mask_fields`](crate::read_mask_fields)
/// (e.g. [`ObjectReadMask`](crate::read_mask_fields::ObjectReadMask)) which
/// are passed directly to the masked client methods. This type is the
/// underlying string holder, useful when composing masks by hand:
///
/// ```
/// use iota_sdk_grpc_client::{ReadMask, read_mask_fields::TransactionField};
/// use iota_sdk_grpc_client::ReadMask;
///
/// // Single field
/// let mask = ReadMask::from(TransactionField::EFFECTS);
/// assert_eq!(mask.as_str(), "effects");
///
/// // Multiple fields
/// let mask = ReadMask::from(&[TransactionField::EFFECTS, TransactionField::CHECKPOINT]);
/// let mask = ReadMask::from("effects,checkpoint");
/// assert_eq!(mask.as_str(), "effects,checkpoint");
/// ```
#[derive(Clone, Debug)]
Expand Down Expand Up @@ -209,11 +207,8 @@ impl From<FieldMask> for ReadMask<'_> {
///
/// This is a convenience helper that handles the common pattern of using
/// a user-provided field mask or falling back to a default.
pub fn field_mask_with_default(custom: Option<ReadMask<'_>>, default: &str) -> FieldMask {
match custom {
Some(mask) => FieldMask::from_str(mask.as_str()),
None => FieldMask::from_str(default),
}
pub fn field_mask_with_default(custom: Option<&str>, default: &str) -> FieldMask {
FieldMask::from_str(custom.unwrap_or(default))
}

/// Safely convert a `usize` to `u32`, saturating at `u32::MAX` instead of
Expand Down
138 changes: 95 additions & 43 deletions crates/iota-sdk-grpc-client/src/api/execution/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@

//! High-level API for transaction execution.

use iota_grpc_types::v1::{
signatures::{UserSignature as ProtoUserSignature, UserSignatures},
transaction::ExecutedTransaction,
transaction_execution_service::{ExecuteTransactionItem, ExecuteTransactionsRequest},
use iota_grpc_types::{
read_mask_fields::TransactionReadMask,
v1::{
signatures::{UserSignature as ProtoUserSignature, UserSignatures},
transaction::ExecutedTransaction,
transaction_execution_service::{ExecuteTransactionItem, ExecuteTransactionsRequest},
},
};
use iota_types::SignedTransaction;

use crate::{
Client,
api::{
EXECUTE_TRANSACTIONS_READ_MASK, Error, MetadataEnvelope, ProtoResult, ProtocolError,
ReadMask, Result, build_proto_transaction, field_mask_with_default,
Result, build_proto_transaction, field_mask_with_default,
},
};

Expand All @@ -31,36 +34,31 @@ impl Client {
/// - `result.input_objects()` - Get input objects (if requested)
/// - `result.output_objects()` - Get output objects (if requested)
///
/// # Read Mask
///
/// The optional `read_mask` parameter controls which fields the server
/// returns. If `None`, uses [`EXECUTE_TRANSACTIONS_READ_MASK`] which
/// includes effects, events, and input/output objects.
///
/// Use [`TransactionField`](iota_grpc_types::read_mask_fields::TransactionField)
/// constants with [`ReadMask::from`] for field selection.
/// Uses the default field mask [`EXECUTE_TRANSACTIONS_READ_MASK`] which
/// includes effects, events, and input/output objects. Use
/// [`execute_transaction_masked`](Self::execute_transaction_masked) to
/// specify a custom mask.
///
/// # Checkpoint Inclusion
///
/// If `checkpoint_inclusion_timeout_ms` is set, the server will wait up to
/// the specified duration (in milliseconds) for the transaction to be
/// included in a checkpoint before returning. When set, include
/// `checkpoint` and `timestamp` in the `read_mask` to receive the data.
/// included in a checkpoint before returning. When set, callers wanting
/// the checkpoint metadata should use
/// [`execute_transaction_masked`](Self::execute_transaction_masked) with a
/// mask that includes `checkpoint` and `timestamp`.
///
/// # Example
///
/// ```no_run
/// # use iota_sdk_grpc_client::Client;
/// # use iota_types::SignedTransaction;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:9000")?;
/// let client = Client::new_localnet()?;
///
/// let signed_tx: SignedTransaction = todo!();
/// let result = client.execute_transaction(signed_tx, None).await?;
///
/// // Execute transaction - returns proto type
/// let result = client.execute_transaction(signed_tx, None, None).await?;
///
/// // Lazy conversion - only deserialize what you need
/// let effects = result.body().effects()?.effects()?;
/// println!("Status: {:?}", effects.as_v1().status);
///
Expand All @@ -74,20 +72,36 @@ impl Client {
pub async fn execute_transaction(
&self,
signed_transaction: SignedTransaction,
read_mask: Option<ReadMask<'_>>,
checkpoint_inclusion_timeout_ms: Option<u64>,
checkpoint_inclusion_timeout_ms: impl Into<Option<u64>>,
) -> Result<MetadataEnvelope<ExecutedTransaction>> {
self.execute_transactions(
self.execute_transactions_internal(
vec![signed_transaction],
read_mask,
checkpoint_inclusion_timeout_ms,
None,
checkpoint_inclusion_timeout_ms.into(),
)
.await?
.try_map(|results| {
results.into_iter().next().ok_or_else(|| {
Error::Protocol(ProtocolError::EmptyResponseField("transaction_results"))
})?
})
.try_map(extract_single_execution_result)
}

/// Execute a signed transaction, with a custom read mask.
///
/// See [`execute_transaction`](Self::execute_transaction) for behavior.
/// Pass a
/// [`TransactionField`](iota_grpc_types::read_mask_fields::TransactionField)
/// or any slice/array/vec of fields — conversion is automatic.
pub async fn execute_transaction_masked(
&self,
signed_transaction: SignedTransaction,
read_mask: impl Into<TransactionReadMask>,
checkpoint_inclusion_timeout_ms: impl Into<Option<u64>>,
) -> Result<MetadataEnvelope<ExecutedTransaction>> {
self.execute_transactions_internal(
vec![signed_transaction],
Some(read_mask.into()),
checkpoint_inclusion_timeout_ms.into(),
)
.await?
.try_map(extract_single_execution_result)
}

/// Execute a batch of signed transactions.
Expand All @@ -99,22 +113,18 @@ impl Client {
/// input. Each element is either the successfully executed transaction or
/// the per-item error returned by the server.
///
/// # Read Mask
///
/// The optional `read_mask` parameter controls which fields the server
/// returns for each `ExecutedTransaction`. If `None`, uses
/// [`EXECUTE_TRANSACTIONS_READ_MASK`] which includes effects, events, and
/// input/output objects.
///
/// Use [`TransactionField`](iota_grpc_types::read_mask_fields::TransactionField)
/// constants with [`ReadMask::from`] for field selection.
/// Uses the default field mask [`EXECUTE_TRANSACTIONS_READ_MASK`]. Use
/// [`execute_transactions_masked`](Self::execute_transactions_masked) to
/// specify a custom mask.
///
/// # Checkpoint Inclusion
///
/// If `checkpoint_inclusion_timeout_ms` is set, the server will wait up to
/// the specified duration (in milliseconds) for all executed transactions
/// to be included in a checkpoint before returning. When set, include
/// `checkpoint` and `timestamp` in the `read_mask` to receive the data.
/// to be included in a checkpoint before returning. Callers wanting the
/// checkpoint metadata should use
/// [`execute_transactions_masked`](Self::execute_transactions_masked) with
/// a mask that includes `checkpoint` and `timestamp`.
///
/// # Errors
///
Expand All @@ -124,7 +134,40 @@ impl Client {
pub async fn execute_transactions(
&self,
transactions: Vec<SignedTransaction>,
read_mask: Option<ReadMask<'_>>,
checkpoint_inclusion_timeout_ms: impl Into<Option<u64>>,
) -> Result<MetadataEnvelope<Vec<Result<ExecutedTransaction>>>> {
self.execute_transactions_internal(
transactions,
None,
checkpoint_inclusion_timeout_ms.into(),
)
.await
}

/// Execute a batch of signed transactions, with a custom read mask.
///
/// See [`execute_transactions`](Self::execute_transactions) for behavior.
/// Pass a
/// [`TransactionField`](iota_grpc_types::read_mask_fields::TransactionField)
/// or any slice/array/vec of fields — conversion is automatic.
pub async fn execute_transactions_masked(
&self,
transactions: Vec<SignedTransaction>,
read_mask: impl Into<TransactionReadMask>,
checkpoint_inclusion_timeout_ms: impl Into<Option<u64>>,
) -> Result<MetadataEnvelope<Vec<Result<ExecutedTransaction>>>> {
self.execute_transactions_internal(
transactions,
Some(read_mask.into()),
checkpoint_inclusion_timeout_ms.into(),
)
.await
}

async fn execute_transactions_internal(
&self,
transactions: Vec<SignedTransaction>,
read_mask: Option<TransactionReadMask>,
checkpoint_inclusion_timeout_ms: Option<u64>,
) -> Result<MetadataEnvelope<Vec<Result<ExecutedTransaction>>>> {
if transactions.is_empty() {
Expand All @@ -139,7 +182,7 @@ impl Client {
let mut request = ExecuteTransactionsRequest::default()
.with_transactions(items)
.with_read_mask(field_mask_with_default(
read_mask,
read_mask.as_ref().map(|m| m.as_str()),
EXECUTE_TRANSACTIONS_READ_MASK,
));

Expand All @@ -161,6 +204,15 @@ impl Client {
}
}

fn extract_single_execution_result(
results: Vec<Result<ExecutedTransaction>>,
) -> Result<ExecutedTransaction> {
results
.into_iter()
.next()
.ok_or_else(|| Error::Protocol(ProtocolError::EmptyResponseField("transaction_results")))?
}

/// Convert a `SignedTransaction` into a proto `ExecuteTransactionItem`.
fn build_execute_item(signed_transaction: SignedTransaction) -> Result<ExecuteTransactionItem> {
let tx_digest = signed_transaction.transaction.digest();
Expand Down
Loading
Loading