Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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