miden_client/rpc/domain/
transaction.rs1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use miden_protocol::Word;
6use miden_protocol::asset::Asset;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{NoteHeader, NoteId, NoteInclusionProof, 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#[cfg(test)]
21pub const ACCOUNT_ID_NATIVE_ASSET_FAUCET: u128 = 0xab00_0000_0000_cd21_0000_ac00_0000_de00_u128;
22
23impl 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#[derive(Debug, Clone)]
61pub struct TransactionRecord {
62 pub block_num: BlockNumber,
64 pub transaction_header: TransactionHeader,
66 pub output_notes: Vec<CommittedNote>,
69 pub erased_output_notes: Vec<NoteHeader>,
71}
72
73impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
74 type Error = RpcError;
75
76 fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
77 let block_num = value.block_num.into();
78 let proto_header =
79 value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
80 entity: "TransactionRecord",
81 field_name: "transaction_header",
82 })?;
83
84 let (transaction_header, output_notes, erased_output_notes) =
85 convert_transaction_header(proto_header, value.output_note_proofs)?;
86
87 Ok(Self {
88 block_num,
89 transaction_header,
90 output_notes,
91 erased_output_notes,
92 })
93 }
94}
95
96fn convert_transaction_header(
104 value: proto::transaction::TransactionHeader,
105 output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
106) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteHeader>), RpcError> {
107 let account_id =
108 value
109 .account_id
110 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
111 entity: "TransactionHeader",
112 field_name: "account_id",
113 })?;
114
115 let initial_state_commitment = value.initial_state_commitment.ok_or(
116 RpcConversionError::MissingFieldInProtobufRepresentation {
117 entity: "TransactionHeader",
118 field_name: "initial_state_commitment",
119 },
120 )?;
121
122 let final_state_commitment = value.final_state_commitment.ok_or(
123 RpcConversionError::MissingFieldInProtobufRepresentation {
124 entity: "TransactionHeader",
125 field_name: "final_state_commitment",
126 },
127 )?;
128
129 let note_commitments = value
130 .input_notes
131 .into_iter()
132 .map(|d| {
133 let word: Word = d
134 .nullifier
135 .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
136 .try_into()
137 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
138 Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
139 })
140 .collect::<Result<Vec<_>, RpcError>>()?;
141 let input_notes = InputNotes::new_unchecked(note_commitments);
142
143 let output_note_headers: Vec<NoteHeader> = value
145 .output_notes
146 .into_iter()
147 .map(|proto_header| {
148 proto_header
149 .try_into()
150 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
151 })
152 .collect::<Result<Vec<_>, RpcError>>()?;
153
154 let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
156 for mut proto_proof in output_note_proofs {
157 let note_id: NoteId = proto_proof
158 .note_id
159 .take()
160 .ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
161 .try_into()
162 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
163 let inclusion_proof: NoteInclusionProof = proto_proof
164 .try_into()
165 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
166 proof_map.insert(note_id, inclusion_proof);
167 }
168
169 let mut committed_output_notes = Vec::with_capacity(proof_map.len());
171 let mut erased_output_notes =
172 Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
173
174 for header in &output_note_headers {
175 let note_id = header.id();
176 if let Some(proof) = proof_map.remove(¬e_id) {
177 committed_output_notes.push(CommittedNote::new(note_id, *header.metadata(), proof));
178 } else {
179 erased_output_notes.push(*header);
180 }
181 }
182
183 let fee_asset: Asset = value
184 .fee
185 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
186 entity: "TransactionHeader",
187 field_name: "fee",
188 })?
189 .try_into()?;
190
191 let fee = match fee_asset {
192 Asset::Fungible(fungible) => fungible,
193 Asset::NonFungible(_) => {
194 return Err(RpcError::InvalidResponse(
195 "expected fungible asset for transaction fee".into(),
196 ));
197 },
198 };
199
200 let transaction_header = TransactionHeader::new(
201 account_id.try_into()?,
202 initial_state_commitment.try_into()?,
203 final_state_commitment.try_into()?,
204 input_notes,
205 output_note_headers,
206 fee,
207 );
208 Ok((transaction_header, committed_output_notes, erased_output_notes))
209}