Skip to main content

miden_client/rpc/domain/
transaction.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::AccountId;
6use miden_protocol::asset::FungibleAsset;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{NoteHeader, Nullifier};
9use miden_protocol::transaction::{
10    InputNoteCommitment,
11    InputNotes,
12    TransactionHeader,
13    TransactionId,
14};
15
16use crate::rpc::{RpcConversionError, RpcError, generated as proto};
17
18// TODO: Remove this when we turn on fees and the node informs the correct asset account ID
19
20/// A native asset faucet ID for use in testing scenarios.
21pub const ACCOUNT_ID_NATIVE_ASSET_FAUCET: u128 = 0xab00_0000_0000_cd20_0000_ac00_0000_de00_u128;
22
23// INTO TRANSACTION ID
24// ================================================================================================
25
26impl TryFrom<proto::primitives::Digest> for TransactionId {
27    type Error = RpcConversionError;
28
29    fn try_from(value: proto::primitives::Digest) -> Result<Self, Self::Error> {
30        let word: Word = value.try_into()?;
31        Ok(Self::from_raw(word))
32    }
33}
34
35impl TryFrom<proto::transaction::TransactionId> for TransactionId {
36    type Error = RpcConversionError;
37
38    fn try_from(value: proto::transaction::TransactionId) -> Result<Self, Self::Error> {
39        value
40            .id
41            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
42                entity: "TransactionId",
43                field_name: "id",
44            })?
45            .try_into()
46    }
47}
48
49impl From<TransactionId> for proto::transaction::TransactionId {
50    fn from(value: TransactionId) -> Self {
51        Self { id: Some(value.as_word().into()) }
52    }
53}
54
55// TRANSACTION INCLUSION
56// ================================================================================================
57
58/// Represents a transaction that was included in the node at a certain block.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct TransactionInclusion {
61    /// The transaction identifier.
62    pub transaction_id: TransactionId,
63    /// The number of the block in which the transaction was included.
64    pub block_num: BlockNumber,
65    /// The account that the transaction was executed against.
66    pub account_id: AccountId,
67    /// The initial account state commitment before the transaction was executed.
68    pub initial_state_commitment: Word,
69}
70
71// TRANSACTIONS INFO
72// ================================================================================================
73
74/// Represent a list of transaction records that were included in a range of blocks.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct TransactionsInfo {
77    /// Current chain tip
78    pub chain_tip: BlockNumber,
79    /// The block number of the last check included in this response.
80    pub block_num: BlockNumber,
81    /// List of transaction records.
82    pub transaction_records: Vec<TransactionRecord>,
83}
84
85impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
86    type Error = RpcError;
87
88    fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
89        let pagination_info = value.pagination_info.ok_or(
90            RpcConversionError::MissingFieldInProtobufRepresentation {
91                entity: "SyncTransactionsResponse",
92                field_name: "pagination_info",
93            },
94        )?;
95
96        let chain_tip = pagination_info.chain_tip.into();
97        let block_num = pagination_info.block_num.into();
98
99        let transaction_records = value
100            .transactions
101            .into_iter()
102            .map(TryInto::try_into)
103            .collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
104
105        Ok(Self {
106            chain_tip,
107            block_num,
108            transaction_records,
109        })
110    }
111}
112
113// TRANSACTION RECORD
114// ================================================================================================
115
116/// Contains information about a transaction that got included in the chain at a specific block
117/// number.
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct TransactionRecord {
120    /// Block number in which the transaction was included.
121    pub block_num: BlockNumber,
122    /// A transaction header.
123    pub transaction_header: TransactionHeader,
124}
125
126impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
127    type Error = RpcError;
128
129    fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
130        let block_num = value.block_num.into();
131        let transaction_header =
132            value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
133                entity: "TransactionRecord",
134                field_name: "transaction_header",
135            })?;
136
137        Ok(Self {
138            block_num,
139            transaction_header: transaction_header.try_into()?,
140        })
141    }
142}
143
144impl TryFrom<proto::transaction::TransactionHeader> for TransactionHeader {
145    type Error = RpcError;
146
147    fn try_from(value: proto::transaction::TransactionHeader) -> Result<Self, Self::Error> {
148        let account_id =
149            value
150                .account_id
151                .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
152                    entity: "TransactionHeader",
153                    field_name: "account_id",
154                })?;
155
156        let initial_state_commitment = value.initial_state_commitment.ok_or(
157            RpcConversionError::MissingFieldInProtobufRepresentation {
158                entity: "TransactionHeader",
159                field_name: "initial_state_commitment",
160            },
161        )?;
162
163        let final_state_commitment = value.final_state_commitment.ok_or(
164            RpcConversionError::MissingFieldInProtobufRepresentation {
165                entity: "TransactionHeader",
166                field_name: "final_state_commitment",
167            },
168        )?;
169
170        let note_commitments = value
171            .input_notes
172            .into_iter()
173            .map(|input_note| {
174                let nullifier_digest = input_note.nullifier.ok_or(
175                    RpcConversionError::MissingFieldInProtobufRepresentation {
176                        entity: "InputNoteCommitment",
177                        field_name: "nullifier",
178                    },
179                )?;
180                let word: Word = nullifier_digest
181                    .try_into()
182                    .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
183                Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
184            })
185            .collect::<Result<Vec<_>, RpcError>>()?;
186        let input_notes = InputNotes::new_unchecked(note_commitments);
187
188        let output_notes = value
189            .output_notes
190            .into_iter()
191            .map(NoteHeader::try_from)
192            .collect::<Result<Vec<NoteHeader>, RpcError>>()?;
193
194        let transaction_header = TransactionHeader::new(
195            account_id.try_into()?,
196            initial_state_commitment.try_into()?,
197            final_state_commitment.try_into()?,
198            input_notes,
199            output_notes,
200            // TODO: handle this; should we open an issue in miden-node?
201            FungibleAsset::new(ACCOUNT_ID_NATIVE_ASSET_FAUCET.try_into().expect("is valid"), 0u64)
202                .unwrap(),
203        );
204        Ok(transaction_header)
205    }
206}
207
208impl TryFrom<proto::note::NoteHeader> for NoteHeader {
209    type Error = RpcError;
210
211    fn try_from(value: proto::note::NoteHeader) -> Result<Self, Self::Error> {
212        let note_id = value
213            .note_id
214            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
215                entity: "NoteHeader",
216                field_name: "note_id",
217            })?
218            .try_into()?;
219
220        let note_metadata = value
221            .metadata
222            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
223                entity: "NoteHeader",
224                field_name: "metadata",
225            })?
226            .try_into()?;
227
228        Ok(Self::new(note_id, note_metadata))
229    }
230}
231
232impl TryFrom<proto::note::NoteSyncRecord> for NoteHeader {
233    type Error = RpcError;
234
235    fn try_from(value: proto::note::NoteSyncRecord) -> Result<Self, Self::Error> {
236        let note_id = value
237            .note_id
238            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
239                entity: "NoteSyncRecord",
240                field_name: "note_id",
241            })?
242            .try_into()?;
243
244        let note_metadata = value
245            .metadata
246            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
247                entity: "NoteSyncRecord",
248                field_name: "metadata",
249            })?
250            .try_into()?;
251
252        let note_header = Self::new(note_id, note_metadata);
253        Ok(note_header)
254    }
255}