Skip to main content

near_kit/types/
wait_level.rs

1//! Type-safe wait levels for transaction submission.
2//!
3//! Instead of using a runtime enum, each wait level is its own type with an
4//! associated `Response` type. This lets the compiler determine the return type
5//! of `send().wait_until(...)` based on which marker you pass in:
6//!
7//! ```rust,no_run
8//! # use near_kit::*;
9//! # async fn example(near: &Near) -> Result<(), Error> {
10//! // Default — returns FinalExecutionOutcome
11//! near.transfer("bob.testnet", NearToken::from_near(1)).await?;
12//!
13//! // Executed levels — also returns FinalExecutionOutcome
14//! near.transfer("bob.testnet", NearToken::from_near(1))
15//!     .wait_until(Final)
16//!     .await?;
17//!
18//! // Non-executed levels — returns SendTxResponse (hash + sender)
19//! let response = near.transfer("bob.testnet", NearToken::from_near(1))
20//!     .wait_until(Included)
21//!     .await?;
22//! println!("tx hash: {}", response.transaction_hash);
23//! # Ok(())
24//! # }
25//! ```
26
27use crate::error::Error;
28use crate::types::AccountId;
29
30use super::block_reference::TxExecutionStatus;
31use super::rpc::{FinalExecutionOutcome, RawTransactionResponse, SendTxResponse};
32
33mod sealed {
34    pub trait Sealed {}
35}
36
37/// Trait for type-safe transaction wait levels.
38///
39/// Each wait level is a zero-sized marker type that carries:
40/// - The [`TxExecutionStatus`] to send to the RPC
41/// - An associated [`Response`](WaitLevel::Response) type that determines
42///   what `send().wait_until(...)` returns
43///
44/// This trait is sealed and cannot be implemented outside this crate.
45pub trait WaitLevel: sealed::Sealed + Send + Sync + 'static {
46    /// The type returned when awaiting a transaction with this wait level.
47    type Response: Send + 'static;
48
49    /// The RPC wait_until value.
50    fn status() -> TxExecutionStatus;
51
52    /// Convert a raw RPC response into the appropriate return type.
53    ///
54    /// For executed levels, this extracts the outcome and checks for
55    /// `InvalidTxError`. For non-executed levels, this builds a
56    /// [`SendTxResponse`] with the transaction hash and sender ID.
57    #[doc(hidden)]
58    fn convert(
59        response: RawTransactionResponse,
60        sender_id: &AccountId,
61    ) -> Result<Self::Response, Error>;
62}
63
64// =============================================================================
65// Non-executed wait levels → SendTxResponse
66// =============================================================================
67
68/// Don't wait, return immediately after the RPC accepts the transaction.
69///
70/// Returns [`SendTxResponse`] (no execution outcome available).
71///
72/// Named `Submitted` instead of `None` to avoid shadowing `Option::None`.
73#[derive(Clone, Copy, Debug)]
74pub struct Submitted;
75
76impl sealed::Sealed for Submitted {}
77impl WaitLevel for Submitted {
78    type Response = SendTxResponse;
79    fn status() -> TxExecutionStatus {
80        TxExecutionStatus::None
81    }
82    fn convert(
83        response: RawTransactionResponse,
84        sender_id: &AccountId,
85    ) -> Result<Self::Response, Error> {
86        Ok(SendTxResponse {
87            transaction_hash: response.transaction_hash,
88            sender_id: sender_id.clone(),
89        })
90    }
91}
92
93/// Wait for the transaction to be included in a block.
94///
95/// Returns [`SendTxResponse`] (no execution outcome available).
96#[derive(Clone, Copy, Debug)]
97pub struct Included;
98
99impl sealed::Sealed for Included {}
100impl WaitLevel for Included {
101    type Response = SendTxResponse;
102    fn status() -> TxExecutionStatus {
103        TxExecutionStatus::Included
104    }
105    fn convert(
106        response: RawTransactionResponse,
107        sender_id: &AccountId,
108    ) -> Result<Self::Response, Error> {
109        Ok(SendTxResponse {
110            transaction_hash: response.transaction_hash,
111            sender_id: sender_id.clone(),
112        })
113    }
114}
115
116/// Wait for the transaction's block to reach finality.
117///
118/// Returns [`SendTxResponse`] (no execution outcome available).
119#[derive(Clone, Copy, Debug)]
120pub struct IncludedFinal;
121
122impl sealed::Sealed for IncludedFinal {}
123impl WaitLevel for IncludedFinal {
124    type Response = SendTxResponse;
125    fn status() -> TxExecutionStatus {
126        TxExecutionStatus::IncludedFinal
127    }
128    fn convert(
129        response: RawTransactionResponse,
130        sender_id: &AccountId,
131    ) -> Result<Self::Response, Error> {
132        Ok(SendTxResponse {
133            transaction_hash: response.transaction_hash,
134            sender_id: sender_id.clone(),
135        })
136    }
137}
138
139// =============================================================================
140// Executed wait levels → FinalExecutionOutcome
141// =============================================================================
142
143/// Extract and validate the execution outcome from a raw response.
144fn extract_outcome(
145    response: RawTransactionResponse,
146    level: &str,
147) -> Result<FinalExecutionOutcome, Error> {
148    let outcome = response.outcome.ok_or_else(|| {
149        Error::InvalidTransaction(format!(
150            "RPC returned no execution outcome for transaction {} at wait level {}",
151            response.transaction_hash, level,
152        ))
153    })?;
154
155    use super::error::TxExecutionError;
156    use super::rpc::FinalExecutionStatus;
157    match outcome.status {
158        FinalExecutionStatus::Failure(TxExecutionError::InvalidTxError(e)) => {
159            Err(Error::InvalidTx(Box::new(e)))
160        }
161        _ => Ok(outcome),
162    }
163}
164
165/// Wait for execution (optimistic, not yet finalized).
166///
167/// Returns [`FinalExecutionOutcome`]. This is the default when using
168/// `.send().await` without specifying a wait level.
169#[derive(Clone, Copy, Debug)]
170pub struct ExecutedOptimistic;
171
172impl sealed::Sealed for ExecutedOptimistic {}
173impl WaitLevel for ExecutedOptimistic {
174    type Response = FinalExecutionOutcome;
175    fn status() -> TxExecutionStatus {
176        TxExecutionStatus::ExecutedOptimistic
177    }
178    fn convert(
179        response: RawTransactionResponse,
180        _sender_id: &AccountId,
181    ) -> Result<Self::Response, Error> {
182        extract_outcome(response, "ExecutedOptimistic")
183    }
184}
185
186/// Wait for execution in a finalized block.
187///
188/// Returns [`FinalExecutionOutcome`].
189#[derive(Clone, Copy, Debug)]
190pub struct Executed;
191
192impl sealed::Sealed for Executed {}
193impl WaitLevel for Executed {
194    type Response = FinalExecutionOutcome;
195    fn status() -> TxExecutionStatus {
196        TxExecutionStatus::Executed
197    }
198    fn convert(
199        response: RawTransactionResponse,
200        _sender_id: &AccountId,
201    ) -> Result<Self::Response, Error> {
202        extract_outcome(response, "Executed")
203    }
204}
205
206/// Wait for full finality (all receipts executed, all blocks finalized).
207///
208/// Returns [`FinalExecutionOutcome`].
209#[derive(Clone, Copy, Debug)]
210pub struct Final;
211
212impl sealed::Sealed for Final {}
213impl WaitLevel for Final {
214    type Response = FinalExecutionOutcome;
215    fn status() -> TxExecutionStatus {
216        TxExecutionStatus::Final
217    }
218    fn convert(
219        response: RawTransactionResponse,
220        _sender_id: &AccountId,
221    ) -> Result<Self::Response, Error> {
222        extract_outcome(response, "Final")
223    }
224}