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::Asset;
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 super::note::CommittedNote;
17use crate::rpc::{RpcConversionError, RpcError, generated as proto};
18
19/// A native asset faucet ID for use in testing scenarios.
20#[cfg(test)]
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)]
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    /// The nullifiers of the input notes consumed by this transaction.
70    pub nullifiers: Vec<Nullifier>,
71    /// Output notes from this transaction, with inclusion proofs.
72    pub output_notes: Vec<CommittedNote>,
73}
74
75// TRANSACTIONS INFO
76// ================================================================================================
77
78/// Represent a list of transaction records that were included in a range of blocks.
79#[derive(Debug, Clone)]
80pub struct TransactionsInfo {
81    /// Current chain tip
82    pub chain_tip: BlockNumber,
83    /// The block number of the last check included in this response.
84    pub block_num: BlockNumber,
85    /// List of transaction records.
86    pub transaction_records: Vec<TransactionRecord>,
87}
88
89impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
90    type Error = RpcError;
91
92    fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
93        let pagination_info = value.pagination_info.ok_or(
94            RpcConversionError::MissingFieldInProtobufRepresentation {
95                entity: "SyncTransactionsResponse",
96                field_name: "pagination_info",
97            },
98        )?;
99
100        let chain_tip = pagination_info.chain_tip.into();
101        let block_num = pagination_info.block_num.into();
102
103        let transaction_records = value
104            .transactions
105            .into_iter()
106            .map(TryInto::try_into)
107            .collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
108
109        Ok(Self {
110            chain_tip,
111            block_num,
112            transaction_records,
113        })
114    }
115}
116
117// TRANSACTION RECORD
118// ================================================================================================
119
120/// Contains information about a transaction that got included in the chain at a specific block
121/// number.
122#[derive(Debug, Clone)]
123pub struct TransactionRecord {
124    /// Block number in which the transaction was included.
125    pub block_num: BlockNumber,
126    /// A transaction header.
127    pub transaction_header: TransactionHeader,
128    /// Output notes with inclusion proofs, as returned by the node's `SyncTransactions`
129    /// response.
130    pub output_notes: Vec<CommittedNote>,
131}
132
133impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
134    type Error = RpcError;
135
136    fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
137        let block_num = value.block_num.into();
138        let proto_header =
139            value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
140                entity: "TransactionRecord",
141                field_name: "transaction_header",
142            })?;
143
144        let (transaction_header, output_notes) = convert_transaction_header(proto_header)?;
145
146        Ok(Self {
147            block_num,
148            transaction_header,
149            output_notes,
150        })
151    }
152}
153
154/// Converts a proto `TransactionHeader` into the domain `TransactionHeader` and extracts
155/// committed output notes with their inclusion proofs.
156///
157/// The proto `output_notes` field contains `NoteSyncRecord`s (metadata header + inclusion
158/// proof). We parse each into a `CommittedNote` for output note state transitions, and
159/// also construct `NoteHeader`s for the `TransactionHeader` (which needs them for
160/// identification purposes).
161fn convert_transaction_header(
162    value: proto::transaction::TransactionHeader,
163) -> Result<(TransactionHeader, Vec<CommittedNote>), RpcError> {
164    let account_id =
165        value
166            .account_id
167            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
168                entity: "TransactionHeader",
169                field_name: "account_id",
170            })?;
171
172    let initial_state_commitment = value.initial_state_commitment.ok_or(
173        RpcConversionError::MissingFieldInProtobufRepresentation {
174            entity: "TransactionHeader",
175            field_name: "initial_state_commitment",
176        },
177    )?;
178
179    let final_state_commitment = value.final_state_commitment.ok_or(
180        RpcConversionError::MissingFieldInProtobufRepresentation {
181            entity: "TransactionHeader",
182            field_name: "final_state_commitment",
183        },
184    )?;
185
186    let note_commitments = value
187        .input_notes
188        .into_iter()
189        .map(|d| {
190            let word: Word = d
191                .nullifier
192                .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
193                .try_into()
194                .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
195            Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
196        })
197        .collect::<Result<Vec<_>, RpcError>>()?;
198    let input_notes = InputNotes::new_unchecked(note_commitments);
199
200    // Parse output notes as CommittedNotes (with inclusion proofs) and build NoteHeaders
201    // for the TransactionHeader in a single pass. Notes with attachments may lack full
202    // metadata; they are omitted from the TransactionHeader but still carried as
203    // CommittedNotes for output note state transitions.
204    let mut committed_output_notes = Vec::with_capacity(value.output_notes.len());
205    let mut output_note_headers = Vec::with_capacity(value.output_notes.len());
206
207    for record in value.output_notes {
208        let note = CommittedNote::try_from(record).map_err(RpcError::from)?;
209        if let Some(metadata) = note.metadata() {
210            output_note_headers.push(NoteHeader::new(*note.note_id(), metadata.clone()));
211        }
212        committed_output_notes.push(note);
213    }
214
215    let fee_asset: Asset = value
216        .fee
217        .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
218            entity: "TransactionHeader",
219            field_name: "fee",
220        })?
221        .try_into()?;
222
223    let fee = match fee_asset {
224        Asset::Fungible(fungible) => fungible,
225        Asset::NonFungible(_) => {
226            return Err(RpcError::InvalidResponse(
227                "expected fungible asset for transaction fee".into(),
228            ));
229        },
230    };
231
232    let transaction_header = TransactionHeader::new(
233        account_id.try_into()?,
234        initial_state_commitment.try_into()?,
235        final_state_commitment.try_into()?,
236        input_notes,
237        output_note_headers,
238        fee,
239    );
240    Ok((transaction_header, committed_output_notes))
241}