miden_client/transaction/
record.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3use core::fmt;
4
5use miden_objects::Word;
6use miden_objects::account::AccountId;
7use miden_objects::block::BlockNumber;
8use miden_objects::transaction::{OutputNotes, TransactionId, TransactionScript};
9use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
10
11// TRANSACTION RECORD
12// ================================================================================================
13
14/// Describes a transaction that has been executed and is being tracked on the Client.
15#[derive(Debug, Clone)]
16pub struct TransactionRecord {
17    /// Unique identifier for the transaction.
18    pub id: TransactionId,
19    /// Details associated with the transaction.
20    pub details: TransactionDetails,
21    /// Script associated with the transaction, if no script is provided, only note scripts are
22    /// executed.
23    pub script: Option<TransactionScript>,
24    /// Current status of the transaction.
25    pub status: TransactionStatus,
26}
27
28impl TransactionRecord {
29    /// Creates a new [`TransactionRecord`] instance.
30    pub fn new(
31        id: TransactionId,
32        details: TransactionDetails,
33        script: Option<TransactionScript>,
34        status: TransactionStatus,
35    ) -> TransactionRecord {
36        TransactionRecord { id, details, script, status }
37    }
38
39    /// Updates (if necessary) the transaction status to signify that the transaction was
40    /// committed. Will return true if the record was modified, false otherwise.
41    pub fn commit_transaction(
42        &mut self,
43        commit_height: BlockNumber,
44        commit_timestamp: u64,
45    ) -> bool {
46        match self.status {
47            TransactionStatus::Pending => {
48                self.status = TransactionStatus::Committed {
49                    block_number: commit_height,
50                    commit_timestamp,
51                };
52                true
53            },
54            // TODO: We need a better strategy here. If a transaction was discarded within this
55            // same chain of updates, it would be better to pass the state to committed and then
56            // remove the account invalid states and make them valid again
57            TransactionStatus::Discarded(_) | TransactionStatus::Committed { .. } => false,
58        }
59    }
60
61    /// Updates (if necessary) the transaction status to signify that the transaction was
62    /// discarded. Will return true if the record was modified, false otherwise.
63    pub fn discard_transaction(&mut self, cause: DiscardCause) -> bool {
64        match self.status {
65            TransactionStatus::Pending => {
66                self.status = TransactionStatus::Discarded(cause);
67                true
68            },
69            TransactionStatus::Discarded(_) | TransactionStatus::Committed { .. } => false,
70        }
71    }
72}
73
74/// Describes the details associated with a transaction.
75#[derive(Debug, Clone)]
76pub struct TransactionDetails {
77    /// ID of the account that executed the transaction.
78    pub account_id: AccountId,
79    /// Initial state of the account before the transaction was executed.
80    pub init_account_state: Word,
81    /// Final state of the account after the transaction was executed.
82    pub final_account_state: Word,
83    /// Nullifiers of the input notes consumed in the transaction.
84    pub input_note_nullifiers: Vec<Word>,
85    /// Output notes generated as a result of the transaction.
86    pub output_notes: OutputNotes,
87    /// Block number for the block against which the transaction was executed.
88    pub block_num: BlockNumber,
89    /// Block number at which the transaction was submitted.
90    pub submission_height: BlockNumber,
91    /// Block number at which the transaction is set to expire.
92    pub expiration_block_num: BlockNumber,
93    /// Timestamp indicating when the transaction was created by the client.
94    pub creation_timestamp: u64,
95}
96
97impl Serializable for TransactionDetails {
98    fn write_into<W: ByteWriter>(&self, target: &mut W) {
99        self.account_id.write_into(target);
100        self.init_account_state.write_into(target);
101        self.final_account_state.write_into(target);
102        self.input_note_nullifiers.write_into(target);
103        self.output_notes.write_into(target);
104        self.block_num.write_into(target);
105        self.submission_height.write_into(target);
106        self.expiration_block_num.write_into(target);
107        self.creation_timestamp.write_into(target);
108    }
109}
110
111impl Deserializable for TransactionDetails {
112    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
113        let account_id = AccountId::read_from(source)?;
114        let init_account_state = Word::read_from(source)?;
115        let final_account_state = Word::read_from(source)?;
116        let input_note_nullifiers = Vec::<Word>::read_from(source)?;
117        let output_notes = OutputNotes::read_from(source)?;
118        let block_num = BlockNumber::read_from(source)?;
119        let submission_height = BlockNumber::read_from(source)?;
120        let expiration_block_num = BlockNumber::read_from(source)?;
121        let creation_timestamp = source.read_u64()?;
122
123        Ok(Self {
124            account_id,
125            init_account_state,
126            final_account_state,
127            input_note_nullifiers,
128            output_notes,
129            block_num,
130            submission_height,
131            expiration_block_num,
132            creation_timestamp,
133        })
134    }
135}
136
137/// Represents the cause of the discarded transaction.
138#[derive(Debug, Clone, Copy, PartialEq)]
139pub enum DiscardCause {
140    Expired,
141    InputConsumed,
142    DiscardedInitialState,
143    Stale,
144}
145
146impl DiscardCause {
147    pub fn from_string(cause: &str) -> Result<Self, DeserializationError> {
148        match cause {
149            "Expired" => Ok(DiscardCause::Expired),
150            "InputConsumed" => Ok(DiscardCause::InputConsumed),
151            "DiscardedInitialState" => Ok(DiscardCause::DiscardedInitialState),
152            "Stale" => Ok(DiscardCause::Stale),
153            _ => Err(DeserializationError::InvalidValue(format!("Invalid discard cause: {cause}"))),
154        }
155    }
156}
157
158impl fmt::Display for DiscardCause {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            DiscardCause::Expired => write!(f, "Expired"),
162            DiscardCause::InputConsumed => write!(f, "InputConsumed"),
163            DiscardCause::DiscardedInitialState => write!(f, "DiscardedInitialState"),
164            DiscardCause::Stale => write!(f, "Stale"),
165        }
166    }
167}
168
169impl Serializable for DiscardCause {
170    fn write_into<W: ByteWriter>(&self, target: &mut W) {
171        match self {
172            DiscardCause::Expired => target.write_u8(0),
173            DiscardCause::InputConsumed => target.write_u8(1),
174            DiscardCause::DiscardedInitialState => target.write_u8(2),
175            DiscardCause::Stale => target.write_u8(3),
176        }
177    }
178}
179
180impl Deserializable for DiscardCause {
181    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
182        match source.read_u8()? {
183            0 => Ok(DiscardCause::Expired),
184            1 => Ok(DiscardCause::InputConsumed),
185            2 => Ok(DiscardCause::DiscardedInitialState),
186            3 => Ok(DiscardCause::Stale),
187            _ => Err(DeserializationError::InvalidValue("Invalid discard cause".to_string())),
188        }
189    }
190}
191
192/// Represents the status of a transaction.
193#[derive(Debug, Clone, PartialEq)]
194pub enum TransactionStatus {
195    /// Transaction has been submitted but not yet committed.
196    Pending,
197    /// Transaction has been committed and included at the specified block number.
198    Committed {
199        /// Block number at which the transaction was committed.
200        block_number: BlockNumber,
201        /// Timestamp indicating when the transaction was committed.
202        commit_timestamp: u64,
203    },
204    /// Transaction has been discarded and isn't included in the node.
205    Discarded(DiscardCause),
206}
207
208pub enum TransactionStatusVariant {
209    Pending = 0,
210    Committed = 1,
211    Discarded = 2,
212}
213
214impl TransactionStatus {
215    pub const fn variant(&self) -> TransactionStatusVariant {
216        match self {
217            TransactionStatus::Pending => TransactionStatusVariant::Pending,
218            TransactionStatus::Committed { .. } => TransactionStatusVariant::Committed,
219            TransactionStatus::Discarded(_) => TransactionStatusVariant::Discarded,
220        }
221    }
222}
223
224impl fmt::Display for TransactionStatus {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        match self {
227            TransactionStatus::Pending => write!(f, "Pending"),
228            TransactionStatus::Committed { block_number, .. } => {
229                write!(f, "Committed (Block: {block_number})")
230            },
231            TransactionStatus::Discarded(cause) => write!(f, "Discarded ({cause})",),
232        }
233    }
234}
235
236impl Serializable for TransactionStatus {
237    fn write_into<W: ByteWriter>(&self, target: &mut W) {
238        match self {
239            TransactionStatus::Pending => target.write_u8(self.variant() as u8),
240            TransactionStatus::Committed { block_number, commit_timestamp } => {
241                target.write_u8(self.variant() as u8);
242                block_number.write_into(target);
243                commit_timestamp.write_into(target);
244            },
245            TransactionStatus::Discarded(cause) => {
246                target.write_u8(self.variant() as u8);
247                cause.write_into(target);
248            },
249        }
250    }
251}
252
253impl Deserializable for TransactionStatus {
254    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
255        match source.read_u8()? {
256            variant if variant == TransactionStatusVariant::Pending as u8 => {
257                Ok(TransactionStatus::Pending)
258            },
259            variant if variant == TransactionStatusVariant::Committed as u8 => {
260                let block_number = BlockNumber::read_from(source)?;
261                let commit_timestamp = source.read_u64()?;
262                Ok(TransactionStatus::Committed { block_number, commit_timestamp })
263            },
264            variant if variant == TransactionStatusVariant::Discarded as u8 => {
265                let cause = DiscardCause::read_from(source)?;
266                Ok(TransactionStatus::Discarded(cause))
267            },
268            _ => Err(DeserializationError::InvalidValue("Invalid transaction status".to_string())),
269        }
270    }
271}