Skip to main content

miden_client/rpc/domain/
transaction.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use miden_protocol::Word;
6use miden_protocol::block::BlockNumber;
7use miden_protocol::note::{NoteHeader, NoteId, NoteInclusionProof, Nullifier};
8use miden_protocol::transaction::{
9    InputNoteCommitment,
10    InputNotes,
11    TransactionHeader,
12    TransactionId,
13};
14
15use super::note::CommittedNote;
16use crate::rpc::{RpcConversionError, RpcError, generated as proto};
17
18// INTO TRANSACTION ID
19// ================================================================================================
20
21impl TryFrom<proto::primitives::Digest> for TransactionId {
22    type Error = RpcConversionError;
23
24    fn try_from(value: proto::primitives::Digest) -> Result<Self, Self::Error> {
25        let word: Word = value.try_into()?;
26        Ok(Self::from_raw(word))
27    }
28}
29
30impl TryFrom<proto::transaction::TransactionId> for TransactionId {
31    type Error = RpcConversionError;
32
33    fn try_from(value: proto::transaction::TransactionId) -> Result<Self, Self::Error> {
34        value
35            .id
36            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
37                entity: "TransactionId",
38                field_name: "id",
39            })?
40            .try_into()
41    }
42}
43
44impl From<TransactionId> for proto::transaction::TransactionId {
45    fn from(value: TransactionId) -> Self {
46        Self { id: Some(value.as_word().into()) }
47    }
48}
49
50// TRANSACTION RECORD
51// ================================================================================================
52
53/// Contains information about a transaction that got included in the chain at a specific block
54/// number.
55#[derive(Debug, Clone)]
56pub struct TransactionRecord {
57    /// Block number in which the transaction was included.
58    pub block_num: BlockNumber,
59    /// A transaction header.
60    pub transaction_header: TransactionHeader,
61    /// Output notes with inclusion proofs, as returned by the node's `SyncTransactions`
62    /// response. Does not include erased notes.
63    pub output_notes: Vec<CommittedNote>,
64    /// Output notes that were erased by same-batch note erasure.
65    pub erased_output_notes: Vec<NoteHeader>,
66}
67
68impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
69    type Error = RpcError;
70
71    fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
72        let block_num = value.block_num.into();
73        let proto_header =
74            value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
75                entity: "TransactionRecord",
76                field_name: "transaction_header",
77            })?;
78
79        let (transaction_header, output_notes, erased_output_notes) =
80            convert_transaction_header(proto_header, value.output_note_proofs)?;
81
82        Ok(Self {
83            block_num,
84            transaction_header,
85            output_notes,
86            erased_output_notes,
87        })
88    }
89}
90
91/// Converts a proto `TransactionHeader` and its associated output note inclusion proofs
92/// into the domain `TransactionHeader`, committed output notes, and erased note IDs.
93///
94/// The proto `TransactionHeader.output_notes` contains `NoteHeader`s for ALL output notes
95/// (including erased ones). Inclusion proofs for committed notes are provided separately in
96/// `output_note_proofs`. Notes present in `output_notes` but without a corresponding proof
97/// are erased (created and consumed within the same batch).
98fn convert_transaction_header(
99    value: proto::transaction::TransactionHeader,
100    output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
101) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteHeader>), RpcError> {
102    let account_id =
103        value
104            .account_id
105            .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
106                entity: "TransactionHeader",
107                field_name: "account_id",
108            })?;
109
110    let initial_state_commitment = value.initial_state_commitment.ok_or(
111        RpcConversionError::MissingFieldInProtobufRepresentation {
112            entity: "TransactionHeader",
113            field_name: "initial_state_commitment",
114        },
115    )?;
116
117    let final_state_commitment = value.final_state_commitment.ok_or(
118        RpcConversionError::MissingFieldInProtobufRepresentation {
119            entity: "TransactionHeader",
120            field_name: "final_state_commitment",
121        },
122    )?;
123
124    let note_commitments = value
125        .input_notes
126        .into_iter()
127        .map(|d| {
128            let word: Word = d
129                .nullifier
130                .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
131                .try_into()
132                .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
133            Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
134        })
135        .collect::<Result<Vec<_>, RpcError>>()?;
136    let input_notes = InputNotes::new_unchecked(note_commitments);
137
138    // Parse all output note headers from the transaction header.
139    let output_note_headers: Vec<NoteHeader> = value
140        .output_notes
141        .into_iter()
142        .map(|proto_header| {
143            proto_header
144                .try_into()
145                .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
146        })
147        .collect::<Result<Vec<_>, RpcError>>()?;
148
149    // Build a map of note_id to inclusion_proof from the separate proofs field.
150    let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
151    for mut proto_proof in output_note_proofs {
152        let note_id: NoteId = proto_proof
153            .note_id
154            .take()
155            .ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
156            .try_into()
157            .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
158        let inclusion_proof: NoteInclusionProof = proto_proof
159            .try_into()
160            .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
161        proof_map.insert(note_id, inclusion_proof);
162    }
163
164    // Join: notes with a matching proof are committed; notes without are erased.
165    let mut committed_output_notes = Vec::with_capacity(proof_map.len());
166    let mut erased_output_notes =
167        Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
168
169    for header in &output_note_headers {
170        let note_id = header.id();
171        if let Some(proof) = proof_map.remove(&note_id) {
172            committed_output_notes.push(CommittedNote::new(note_id, *header.metadata(), proof));
173        } else {
174            erased_output_notes.push(*header);
175        }
176    }
177
178    let transaction_header = TransactionHeader::new(
179        account_id.try_into()?,
180        initial_state_commitment.try_into()?,
181        final_state_commitment.try_into()?,
182        input_notes,
183        output_note_headers,
184    );
185    Ok((transaction_header, committed_output_notes, erased_output_notes))
186}