miden_client/rpc/domain/
transaction.rs1use alloc::collections::{BTreeMap, BTreeSet};
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
18impl 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#[derive(Debug, Clone)]
56pub struct TransactionRecord {
57 pub block_num: BlockNumber,
59 pub transaction_header: TransactionHeader,
61 pub output_notes: Vec<CommittedNote>,
64 pub erased_output_notes: Vec<NoteHeader>,
66 pub(crate) consumed_note_refs: Vec<(Nullifier, NoteId)>,
71}
72
73impl TransactionRecord {
74 pub fn trusted_consumed_note_refs(&self) -> impl Iterator<Item = (Nullifier, NoteId)> + '_ {
81 let consumed_nullifiers: BTreeSet<Nullifier> = self
82 .transaction_header
83 .input_notes()
84 .iter()
85 .map(InputNoteCommitment::nullifier)
86 .collect();
87 self.consumed_note_refs
88 .iter()
89 .copied()
90 .filter(move |(nullifier, _)| consumed_nullifiers.contains(nullifier))
91 }
92}
93
94impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
95 type Error = RpcError;
96
97 fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
98 let block_num = value.block_num.into();
99 let proto_header =
100 value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
101 entity: "TransactionRecord",
102 field_name: "transaction_header",
103 })?;
104
105 let (transaction_header, output_notes, erased_output_notes) =
106 convert_transaction_header(proto_header, value.output_note_proofs)?;
107
108 let consumed_note_refs = value
109 .consumed_note_refs
110 .into_iter()
111 .map(|r| {
112 let nullifier: Nullifier = r
113 .nullifier
114 .ok_or(RpcError::ExpectedDataMissing("consumed_note_ref.nullifier".into()))?
115 .try_into()?;
116 let note_id: NoteId = r
117 .note_id
118 .ok_or(RpcError::ExpectedDataMissing("consumed_note_ref.note_id".into()))?
119 .try_into()?;
120 Ok((nullifier, note_id))
121 })
122 .collect::<Result<Vec<_>, RpcError>>()?;
123
124 Ok(Self {
125 block_num,
126 transaction_header,
127 output_notes,
128 erased_output_notes,
129 consumed_note_refs,
130 })
131 }
132}
133
134fn convert_transaction_header(
142 value: proto::transaction::TransactionHeader,
143 output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
144) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteHeader>), RpcError> {
145 let account_id =
146 value
147 .account_id
148 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
149 entity: "TransactionHeader",
150 field_name: "account_id",
151 })?;
152
153 let initial_state_commitment = value.initial_state_commitment.ok_or(
154 RpcConversionError::MissingFieldInProtobufRepresentation {
155 entity: "TransactionHeader",
156 field_name: "initial_state_commitment",
157 },
158 )?;
159
160 let final_state_commitment = value.final_state_commitment.ok_or(
161 RpcConversionError::MissingFieldInProtobufRepresentation {
162 entity: "TransactionHeader",
163 field_name: "final_state_commitment",
164 },
165 )?;
166
167 let note_commitments = value
168 .input_notes
169 .into_iter()
170 .map(|d| {
171 let word: Word = d
172 .nullifier
173 .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
174 .try_into()
175 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
176 Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
177 })
178 .collect::<Result<Vec<_>, RpcError>>()?;
179 let input_notes = InputNotes::new_unchecked(note_commitments);
180
181 let output_note_headers: Vec<NoteHeader> = value
183 .output_notes
184 .into_iter()
185 .map(|proto_header| {
186 proto_header
187 .try_into()
188 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
189 })
190 .collect::<Result<Vec<_>, RpcError>>()?;
191
192 let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
194 for mut proto_proof in output_note_proofs {
195 let note_id: NoteId = proto_proof
196 .note_id
197 .take()
198 .ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
199 .try_into()
200 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
201 let inclusion_proof: NoteInclusionProof = proto_proof
202 .try_into()
203 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
204 proof_map.insert(note_id, inclusion_proof);
205 }
206
207 let mut committed_output_notes = Vec::with_capacity(proof_map.len());
209 let mut erased_output_notes =
210 Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
211
212 for header in &output_note_headers {
213 let note_id = header.id();
214 if let Some(proof) = proof_map.remove(¬e_id) {
215 committed_output_notes.push(CommittedNote::new(note_id, *header.metadata(), proof));
216 } else {
217 erased_output_notes.push(*header);
218 }
219 }
220
221 let transaction_header = TransactionHeader::new(
222 account_id.try_into()?,
223 initial_state_commitment.try_into()?,
224 final_state_commitment.try_into()?,
225 input_notes,
226 output_note_headers,
227 );
228 Ok((transaction_header, committed_output_notes, erased_output_notes))
229}