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}
151
152impl DiscardCause {
153    pub fn from_string(cause: &str) -> Result<Self, DeserializationError> {
154        match cause {
155            "Expired" => Ok(DiscardCause::Expired),
156            "InputConsumed" => Ok(DiscardCause::InputConsumed),
157            "DiscardedInitialState" => Ok(DiscardCause::DiscardedInitialState),
158            "Stale" => Ok(DiscardCause::Stale),
159            _ => Err(DeserializationError::InvalidValue(format!("Invalid discard cause: {cause}"))),
160        }
161    }
162}
163
164impl fmt::Display for DiscardCause {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        match self {
167            DiscardCause::Expired => write!(f, "Expired"),
168            DiscardCause::InputConsumed => write!(f, "InputConsumed"),
169            DiscardCause::DiscardedInitialState => write!(f, "DiscardedInitialState"),
170            DiscardCause::Stale => write!(f, "Stale"),
171        }
172    }
173}
174
175impl Serializable for DiscardCause {
176    fn write_into<W: ByteWriter>(&self, target: &mut W) {
177        match self {
178            DiscardCause::Expired => target.write_u8(0),
179            DiscardCause::InputConsumed => target.write_u8(1),
180            DiscardCause::DiscardedInitialState => target.write_u8(2),
181            DiscardCause::Stale => target.write_u8(3),
182        }
183    }
184}
185
186impl Deserializable for DiscardCause {
187    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
188        match source.read_u8()? {
189            0 => Ok(DiscardCause::Expired),
190            1 => Ok(DiscardCause::InputConsumed),
191            2 => Ok(DiscardCause::DiscardedInitialState),
192            3 => Ok(DiscardCause::Stale),
193            _ => Err(DeserializationError::InvalidValue("Invalid discard cause".to_string())),
194        }
195    }
196}
197
198/// Represents the status of a transaction.
199#[derive(Debug, Clone, PartialEq)]
200pub enum TransactionStatus {
201    /// Transaction has been submitted but not yet committed.
202    Pending,
203    /// Transaction has been committed and included at the specified block number.
204    Committed {
205        /// Block number at which the transaction was committed.
206        block_number: BlockNumber,
207        /// Timestamp indicating when the transaction was committed.
208        commit_timestamp: u64,
209    },
210    /// Transaction has been discarded and isn't included in the node.
211    Discarded(DiscardCause),
212}
213
214pub enum TransactionStatusVariant {
215    Pending = 0,
216    Committed = 1,
217    Discarded = 2,
218}
219
220impl TransactionStatus {
221    pub const fn variant(&self) -> TransactionStatusVariant {
222        match self {
223            TransactionStatus::Pending => TransactionStatusVariant::Pending,
224            TransactionStatus::Committed { .. } => TransactionStatusVariant::Committed,
225            TransactionStatus::Discarded(_) => TransactionStatusVariant::Discarded,
226        }
227    }
228}
229
230impl fmt::Display for TransactionStatus {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        match self {
233            TransactionStatus::Pending => write!(f, "Pending"),
234            TransactionStatus::Committed { block_number, .. } => {
235                write!(f, "Committed (Block: {block_number})")
236            },
237            TransactionStatus::Discarded(cause) => write!(f, "Discarded ({cause})"),
238        }
239    }
240}
241
242impl Serializable for TransactionStatus {
243    fn write_into<W: ByteWriter>(&self, target: &mut W) {
244        match self {
245            TransactionStatus::Pending => target.write_u8(self.variant() as u8),
246            TransactionStatus::Committed { block_number, commit_timestamp } => {
247                target.write_u8(self.variant() as u8);
248                block_number.write_into(target);
249                commit_timestamp.write_into(target);
250            },
251            TransactionStatus::Discarded(cause) => {
252                target.write_u8(self.variant() as u8);
253                cause.write_into(target);
254            },
255        }
256    }
257}
258
259impl Deserializable for TransactionStatus {
260    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
261        match source.read_u8()? {
262            variant if variant == TransactionStatusVariant::Pending as u8 => {
263                Ok(TransactionStatus::Pending)
264            },
265            variant if variant == TransactionStatusVariant::Committed as u8 => {
266                let block_number = BlockNumber::read_from(source)?;
267                let commit_timestamp = source.read_u64()?;
268                Ok(TransactionStatus::Committed { block_number, commit_timestamp })
269            },
270            variant if variant == TransactionStatusVariant::Discarded as u8 => {
271                let cause = DiscardCause::read_from(source)?;
272                Ok(TransactionStatus::Discarded(cause))
273            },
274            _ => Err(DeserializationError::InvalidValue("Invalid transaction status".to_string())),
275        }
276    }
277}