Skip to main content

miden_client/transaction/
record.rs

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