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::account::AccountId;
7use miden_protocol::asset::Asset;
8use miden_protocol::block::BlockNumber;
9use miden_protocol::note::{NoteHeader, NoteId, NoteInclusionProof, Nullifier};
10use miden_protocol::transaction::{
11 InputNoteCommitment,
12 InputNotes,
13 TransactionHeader,
14 TransactionId,
15};
16
17use super::note::{CommittedNote, CommittedNoteMetadata};
18use crate::rpc::{RpcConversionError, RpcError, generated as proto};
19
20#[cfg(test)]
22pub const ACCOUNT_ID_NATIVE_ASSET_FAUCET: u128 = 0xab00_0000_0000_cd20_0000_ac00_0000_de00_u128;
23
24impl TryFrom<proto::primitives::Digest> for TransactionId {
28 type Error = RpcConversionError;
29
30 fn try_from(value: proto::primitives::Digest) -> Result<Self, Self::Error> {
31 let word: Word = value.try_into()?;
32 Ok(Self::from_raw(word))
33 }
34}
35
36impl TryFrom<proto::transaction::TransactionId> for TransactionId {
37 type Error = RpcConversionError;
38
39 fn try_from(value: proto::transaction::TransactionId) -> Result<Self, Self::Error> {
40 value
41 .id
42 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
43 entity: "TransactionId",
44 field_name: "id",
45 })?
46 .try_into()
47 }
48}
49
50impl From<TransactionId> for proto::transaction::TransactionId {
51 fn from(value: TransactionId) -> Self {
52 Self { id: Some(value.as_word().into()) }
53 }
54}
55
56#[derive(Debug, Clone)]
61pub struct TransactionInclusion {
62 pub transaction_id: TransactionId,
64 pub block_num: BlockNumber,
66 pub account_id: AccountId,
68 pub initial_state_commitment: Word,
70 pub nullifiers: Vec<Nullifier>,
72 pub output_notes: Vec<CommittedNote>,
75 pub erased_output_notes: Vec<NoteHeader>,
78}
79
80#[derive(Debug, Clone)]
85pub struct TransactionsInfo {
86 pub chain_tip: BlockNumber,
88 pub block_num: BlockNumber,
90 pub transaction_records: Vec<TransactionRecord>,
92}
93
94impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
95 type Error = RpcError;
96
97 fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
98 let pagination_info = value.pagination_info.ok_or(
99 RpcConversionError::MissingFieldInProtobufRepresentation {
100 entity: "SyncTransactionsResponse",
101 field_name: "pagination_info",
102 },
103 )?;
104
105 let chain_tip = pagination_info.chain_tip.into();
106 let block_num = pagination_info.block_num.into();
107
108 let transaction_records = value
109 .transactions
110 .into_iter()
111 .map(TryInto::try_into)
112 .collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
113
114 Ok(Self {
115 chain_tip,
116 block_num,
117 transaction_records,
118 })
119 }
120}
121
122#[derive(Debug, Clone)]
128pub struct TransactionRecord {
129 pub block_num: BlockNumber,
131 pub transaction_header: TransactionHeader,
133 pub output_notes: Vec<CommittedNote>,
136 pub erased_output_notes: Vec<NoteHeader>,
138}
139
140impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
141 type Error = RpcError;
142
143 fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
144 let block_num = value.block_num.into();
145 let proto_header =
146 value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
147 entity: "TransactionRecord",
148 field_name: "transaction_header",
149 })?;
150
151 let (transaction_header, output_notes, erased_output_notes) =
152 convert_transaction_header(proto_header, value.output_note_proofs)?;
153
154 Ok(Self {
155 block_num,
156 transaction_header,
157 output_notes,
158 erased_output_notes,
159 })
160 }
161}
162
163fn convert_transaction_header(
171 value: proto::transaction::TransactionHeader,
172 output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
173) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteHeader>), RpcError> {
174 let account_id =
175 value
176 .account_id
177 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
178 entity: "TransactionHeader",
179 field_name: "account_id",
180 })?;
181
182 let initial_state_commitment = value.initial_state_commitment.ok_or(
183 RpcConversionError::MissingFieldInProtobufRepresentation {
184 entity: "TransactionHeader",
185 field_name: "initial_state_commitment",
186 },
187 )?;
188
189 let final_state_commitment = value.final_state_commitment.ok_or(
190 RpcConversionError::MissingFieldInProtobufRepresentation {
191 entity: "TransactionHeader",
192 field_name: "final_state_commitment",
193 },
194 )?;
195
196 let note_commitments = value
197 .input_notes
198 .into_iter()
199 .map(|d| {
200 let word: Word = d
201 .nullifier
202 .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
203 .try_into()
204 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
205 Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
206 })
207 .collect::<Result<Vec<_>, RpcError>>()?;
208 let input_notes = InputNotes::new_unchecked(note_commitments);
209
210 let output_note_headers: Vec<NoteHeader> = value
212 .output_notes
213 .into_iter()
214 .map(|proto_header| {
215 proto_header
216 .try_into()
217 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
218 })
219 .collect::<Result<Vec<_>, RpcError>>()?;
220
221 let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
223 for mut proto_proof in output_note_proofs {
224 let note_id: NoteId = proto_proof
225 .note_id
226 .take()
227 .ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
228 .try_into()
229 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
230 let inclusion_proof: NoteInclusionProof = proto_proof
231 .try_into()
232 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
233 proof_map.insert(note_id, inclusion_proof);
234 }
235
236 let mut committed_output_notes = Vec::with_capacity(proof_map.len());
238 let mut erased_output_notes =
239 Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
240
241 for header in &output_note_headers {
242 let note_id = header.id();
243 if let Some(proof) = proof_map.remove(¬e_id) {
244 let metadata = CommittedNoteMetadata::Full(header.metadata().clone());
245 committed_output_notes.push(CommittedNote::new(note_id, metadata, proof));
246 } else {
247 erased_output_notes.push(header.clone());
248 }
249 }
250
251 let fee_asset: Asset = value
252 .fee
253 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
254 entity: "TransactionHeader",
255 field_name: "fee",
256 })?
257 .try_into()?;
258
259 let fee = match fee_asset {
260 Asset::Fungible(fungible) => fungible,
261 Asset::NonFungible(_) => {
262 return Err(RpcError::InvalidResponse(
263 "expected fungible asset for transaction fee".into(),
264 ));
265 },
266 };
267
268 let transaction_header = TransactionHeader::new(
269 account_id.try_into()?,
270 initial_state_commitment.try_into()?,
271 final_state_commitment.try_into()?,
272 input_notes,
273 output_note_headers,
274 fee,
275 );
276 Ok((transaction_header, committed_output_notes, erased_output_notes))
277}