Skip to main content

sui_rpc/client/
transaction_execution.rs

1use super::Client;
2use crate::field::FieldMaskUtil;
3use crate::proto::TryFromProtoError;
4use crate::proto::sui::rpc::v2::ExecuteTransactionRequest;
5use crate::proto::sui::rpc::v2::ExecuteTransactionResponse;
6use crate::proto::sui::rpc::v2::ExecutionError;
7use crate::proto::sui::rpc::v2::GetEpochRequest;
8use crate::proto::sui::rpc::v2::GetTransactionRequest;
9use crate::proto::sui::rpc::v2::GetTransactionResponse;
10use crate::proto::sui::rpc::v2::SubscribeCheckpointsRequest;
11use futures::TryStreamExt;
12use prost_types::FieldMask;
13use std::fmt;
14use std::time::Duration;
15use tonic::Response;
16
17/// Error types that can occur when executing a transaction and waiting for checkpoint
18#[derive(Debug)]
19#[non_exhaustive]
20pub enum ExecuteAndWaitError {
21    /// RPC Error (actual tonic::Status from the client/server)
22    RpcError(tonic::Status),
23    /// Request is missing the required transaction field
24    MissingTransaction,
25    /// Failed to parse/convert the transaction for digest calculation
26    ProtoConversionError(TryFromProtoError),
27    /// Transaction executed but checkpoint wait timed out
28    CheckpointTimeout(Response<ExecuteTransactionResponse>),
29    /// Transaction executed but checkpoint stream had an error
30    CheckpointStreamError {
31        response: Response<ExecuteTransactionResponse>,
32        error: tonic::Status,
33    },
34}
35
36impl std::fmt::Display for ExecuteAndWaitError {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            Self::RpcError(status) => write!(f, "RPC error: {status}"),
40            Self::MissingTransaction => {
41                write!(f, "Request is missing the required transaction field")
42            }
43            Self::ProtoConversionError(e) => write!(f, "Failed to convert transaction: {e}"),
44            Self::CheckpointTimeout(_) => {
45                write!(f, "Transaction executed but checkpoint wait timed out")
46            }
47            Self::CheckpointStreamError { error, .. } => {
48                write!(
49                    f,
50                    "Transaction executed but checkpoint stream had an error: {error}"
51                )
52            }
53        }
54    }
55}
56
57impl std::error::Error for ExecuteAndWaitError {
58    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59        match self {
60            Self::RpcError(status) => Some(status),
61            Self::ProtoConversionError(e) => Some(e),
62            Self::CheckpointStreamError { error, .. } => Some(error),
63            Self::MissingTransaction => None,
64            Self::CheckpointTimeout(_) => None,
65        }
66    }
67}
68
69impl Client {
70    /// Executes a transaction and waits for it to be included in a checkpoint.
71    ///
72    /// This method provides "read your writes" consistency by executing the transaction
73    /// and waiting for it to appear in a checkpoint, which gauruntees indexes have been updated on
74    /// this node.
75    ///
76    /// # Arguments
77    /// * `request` - The transaction execution request (ExecuteTransactionRequest)
78    /// * `timeout` - Maximum time to wait for indexing confirmation
79    ///
80    /// # Returns
81    /// A `Result` containing the response if the transaction was executed and checkpoint confirmed,
82    /// or an error that may still include the response if execution succeeded but checkpoint
83    /// confirmation failed.
84    ///
85    /// # Duplicate submissions
86    /// Submitting a transaction that has already been executed is handled
87    /// gracefully. While the execution RPC is in flight the ledger is probed
88    /// for the transaction, and a transaction that is already in a checkpoint
89    /// is returned without waiting for execution to finish. Likewise, if
90    /// execution fails but the ledger shows the transaction in a checkpoint
91    /// (for example when a resubmission races the original submission), the
92    /// execution error is discarded and the committed transaction is
93    /// returned. In both cases the response is assembled from
94    /// `GetTransaction` using the request's read mask, so it carries the same
95    /// fields an execution response would, with `digest`, `checkpoint`, and
96    /// `timestamp` always populated.
97    pub async fn execute_transaction_and_wait_for_checkpoint(
98        &mut self,
99        request: impl tonic::IntoRequest<ExecuteTransactionRequest>,
100        timeout: Duration,
101    ) -> Result<Response<ExecuteTransactionResponse>, ExecuteAndWaitError> {
102        // Calculate digest from the input transaction to avoid relying on response read mask
103        let request = request.into_request();
104        let transaction = match request.get_ref().transaction_opt() {
105            Some(tx) => tx,
106            None => return Err(ExecuteAndWaitError::MissingTransaction),
107        };
108
109        let executed_txn_digest = match sui_sdk_types::Transaction::try_from(transaction) {
110            Ok(tx) => tx.digest().to_string(),
111            Err(e) => return Err(ExecuteAndWaitError::ProtoConversionError(e)),
112        };
113
114        // Read mask for answering from GetTransaction when execution cannot
115        // provide the response: a duplicate submission that already
116        // committed, or an execution error after the transaction landed.
117        // Both RPCs' masks select fields of `ExecutedTransaction`, so the
118        // caller's mask passes through unchanged; when the caller didn't set
119        // one, mirror ExecuteTransaction's documented default. `digest`,
120        // `checkpoint`, and `timestamp` are always included since this
121        // method's contract populates them.
122        let lookup_mask = {
123            let caller_paths = match &request.get_ref().read_mask {
124                Some(mask) => mask.paths.clone(),
125                None => vec!["effects.status".to_owned()],
126            };
127            FieldMask::from_paths(caller_paths.iter().map(String::as_str).chain([
128                "digest",
129                "checkpoint",
130                "timestamp",
131            ]))
132            .normalize()
133        };
134
135        // Subscribe to checkpoint stream before execution to avoid missing the transaction.
136        // Uses minimal read mask for efficiency since we only nee digest confirmation.
137        // Once server-side filtering is available, we should filter by transaction digest to
138        // further reduce bandwidth.
139        let mut checkpoint_stream = match self
140            .subscription_client()
141            .subscribe_checkpoints(SubscribeCheckpointsRequest::default().with_read_mask(
142                FieldMask::from_str("transactions.digest,sequence_number,summary.timestamp"),
143            ))
144            .await
145        {
146            Ok(stream) => stream.into_inner(),
147            Err(e) => return Err(ExecuteAndWaitError::RpcError(e)),
148        };
149
150        // Scan the subscription for the transaction's digest. Every RPC on
151        // this client shares one HTTP/2 connection, so this future must be
152        // polled concurrently with the execution phase below: a subscription
153        // parked while another call is awaited pins its flow-control window
154        // (checkpoints keep arriving whether or not anyone reads them) and,
155        // past the idle timeout, gets reset by the client's body watchdog.
156        //
157        // Both this future and the execution future below are boxed: their
158        // combined state (two full tonic call chains alive at once) would
159        // otherwise be inlined into this method's future, making it large
160        // enough to threaten a stack overflow in callers that hold it in
161        // deeply nested or spawned futures.
162        let mut scan = Box::pin(async {
163            while let Some(response) = checkpoint_stream.try_next().await? {
164                let checkpoint = response.checkpoint();
165
166                for tx in checkpoint.transactions() {
167                    if tx.digest() == executed_txn_digest {
168                        return Ok((checkpoint.sequence_number(), checkpoint.summary().timestamp));
169                    }
170                }
171            }
172            Err(tonic::Status::aborted(
173                "checkpoint stream ended unexpectedly",
174            ))
175        });
176
177        // The concurrent futures below each need a service client, and a
178        // single `&mut self` cannot back all of them at once, so give each
179        // its own client over the shared channel.
180        let mut execution_client = self.execution_client();
181        let mut post_exec_lookup_client = self.ledger_client();
182        let mut probe_client = self.ledger_client();
183
184        // Execute, then query the fullnode directly to see if it already has
185        // the txn in a checkpoint. This is to handle the case where an
186        // already executed transaction is sent multiple times.
187        let mut exec_and_check = Box::pin(async {
188            let response = execution_client.execute_transaction(request).await?;
189
190            let already_checkpointed = match post_exec_lookup_client
191                .get_transaction(
192                    GetTransactionRequest::default()
193                        .with_digest(&executed_txn_digest)
194                        .with_read_mask(FieldMask::from_str("digest,checkpoint,timestamp")),
195                )
196                .await
197            {
198                Ok(resp) if resp.get_ref().transaction().checkpoint_opt().is_some() => Some((
199                    resp.get_ref().transaction().checkpoint(),
200                    resp.get_ref().transaction().timestamp,
201                )),
202                _ => None,
203            };
204
205            Ok::<_, tonic::Status>((response, already_checkpointed))
206        });
207
208        // Probe the ledger while execution is in flight: a resubmission of a
209        // transaction that already committed can be answered from the ledger
210        // without waiting for, or succeeding at, execution.
211        let mut probe = Box::pin(async {
212            probe_client
213                .get_transaction(
214                    GetTransactionRequest::default()
215                        .with_digest(&executed_txn_digest)
216                        .with_read_mask(lookup_mask.clone()),
217                )
218                .await
219        });
220
221        // Drive execution, the scan, and the probe together. The scan can
222        // complete first (for example, when a duplicate of an already
223        // executed transaction lands in a checkpoint mid-execution), so
224        // remember its outcome; the guards keep completed futures from being
225        // polled again. A probe that finds the transaction in a checkpoint
226        // resolves the call on the spot; any other probe outcome (not found,
227        // not yet checkpointed, or an RPC error) means execution has to
228        // provide the answer.
229        let mut scan_result = None;
230        let mut probe_done = false;
231        let exec_result = loop {
232            tokio::select! {
233                exec = &mut exec_and_check => break exec,
234                result = &mut scan, if scan_result.is_none() => {
235                    scan_result = Some(result);
236                }
237                result = &mut probe, if !probe_done => {
238                    probe_done = true;
239                    if let Ok(lookup) = result
240                        && lookup.get_ref().transaction().checkpoint_opt().is_some()
241                    {
242                        return Ok(lookup_into_execute_response(lookup));
243                    }
244                }
245            }
246        };
247
248        let (mut response, already_checkpointed) = match exec_result {
249            Ok(ok) => ok,
250            Err(error) => {
251                // Execution can fail for a transaction that nonetheless
252                // committed, for example when a resubmission races the
253                // original submission. Consult the ledger before surfacing
254                // the error.
255                drop(probe);
256                if let Ok(lookup) = probe_client
257                    .get_transaction(
258                        GetTransactionRequest::default()
259                            .with_digest(&executed_txn_digest)
260                            .with_read_mask(lookup_mask),
261                    )
262                    .await
263                    && lookup.get_ref().transaction().checkpoint_opt().is_some()
264                {
265                    return Ok(lookup_into_execute_response(lookup));
266                }
267                return Err(ExecuteAndWaitError::RpcError(error));
268            }
269        };
270
271        // Wait for the transaction to appear in a checkpoint, at which point
272        // indexes will have been updated. The direct lookup takes precedence:
273        // when it already places the transaction in a checkpoint there is
274        // nothing to wait for, even if the scan failed in the meantime.
275        let (checkpoint, timestamp) = if let Some(found) = already_checkpointed {
276            found
277        } else {
278            let result = match scan_result {
279                Some(result) => result,
280                None => {
281                    tokio::select! {
282                        result = &mut scan => result,
283                        _ = tokio::time::sleep(timeout) => {
284                            return Err(ExecuteAndWaitError::CheckpointTimeout(response));
285                        }
286                    }
287                }
288            };
289            match result {
290                Ok(found) => found,
291                Err(e) => {
292                    return Err(ExecuteAndWaitError::CheckpointStreamError { response, error: e });
293                }
294            }
295        };
296
297        response
298            .get_mut()
299            .transaction_mut()
300            .set_checkpoint(checkpoint);
301        response.get_mut().transaction_mut().timestamp = timestamp;
302        Ok(response)
303    }
304
305    /// Retrieves the current reference gas price from the latest epoch information.
306    ///
307    /// # Returns
308    /// The reference gas price as a `u64`
309    ///
310    /// # Errors
311    /// Returns an error if there is an RPC error when fetching the epoch information
312    pub async fn get_reference_gas_price(&mut self) -> Result<u64, tonic::Status> {
313        let request = GetEpochRequest::latest()
314            .with_read_mask(FieldMask::from_paths(["reference_gas_price"]));
315        let response = self.ledger_client().get_epoch(request).await?.into_inner();
316        Ok(response.epoch().reference_gas_price())
317    }
318}
319
320/// Builds the response for a transaction answered from the ledger instead of
321/// from execution.
322fn lookup_into_execute_response(
323    response: Response<GetTransactionResponse>,
324) -> Response<ExecuteTransactionResponse> {
325    Response::new(ExecuteTransactionResponse {
326        transaction: response.into_inner().transaction,
327    })
328}
329
330impl fmt::Display for ExecutionError {
331    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        let description = self.description.as_deref().unwrap_or("No description");
333        write!(
334            f,
335            "ExecutionError: Kind: {}, Description: {}",
336            self.kind().as_str_name(),
337            description
338        )
339    }
340}