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_note_ids: Vec<NoteId>,
77}
78
79#[derive(Debug, Clone)]
84pub struct TransactionsInfo {
85 pub chain_tip: BlockNumber,
87 pub block_num: BlockNumber,
89 pub transaction_records: Vec<TransactionRecord>,
91}
92
93impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
94 type Error = RpcError;
95
96 fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
97 let pagination_info = value.pagination_info.ok_or(
98 RpcConversionError::MissingFieldInProtobufRepresentation {
99 entity: "SyncTransactionsResponse",
100 field_name: "pagination_info",
101 },
102 )?;
103
104 let chain_tip = pagination_info.chain_tip.into();
105 let block_num = pagination_info.block_num.into();
106
107 let transaction_records = value
108 .transactions
109 .into_iter()
110 .map(TryInto::try_into)
111 .collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
112
113 Ok(Self {
114 chain_tip,
115 block_num,
116 transaction_records,
117 })
118 }
119}
120
121#[derive(Debug, Clone)]
127pub struct TransactionRecord {
128 pub block_num: BlockNumber,
130 pub transaction_header: TransactionHeader,
132 pub output_notes: Vec<CommittedNote>,
135 pub erased_output_note_ids: Vec<NoteId>,
137}
138
139impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
140 type Error = RpcError;
141
142 fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
143 let block_num = value.block_num.into();
144 let proto_header =
145 value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
146 entity: "TransactionRecord",
147 field_name: "transaction_header",
148 })?;
149
150 let (transaction_header, output_notes, erased_output_note_ids) =
151 convert_transaction_header(proto_header, value.output_note_proofs)?;
152
153 Ok(Self {
154 block_num,
155 transaction_header,
156 output_notes,
157 erased_output_note_ids,
158 })
159 }
160}
161
162fn convert_transaction_header(
170 value: proto::transaction::TransactionHeader,
171 output_note_proofs: Vec<proto::note::NoteInclusionInBlockProof>,
172) -> Result<(TransactionHeader, Vec<CommittedNote>, Vec<NoteId>), RpcError> {
173 let account_id =
174 value
175 .account_id
176 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
177 entity: "TransactionHeader",
178 field_name: "account_id",
179 })?;
180
181 let initial_state_commitment = value.initial_state_commitment.ok_or(
182 RpcConversionError::MissingFieldInProtobufRepresentation {
183 entity: "TransactionHeader",
184 field_name: "initial_state_commitment",
185 },
186 )?;
187
188 let final_state_commitment = value.final_state_commitment.ok_or(
189 RpcConversionError::MissingFieldInProtobufRepresentation {
190 entity: "TransactionHeader",
191 field_name: "final_state_commitment",
192 },
193 )?;
194
195 let note_commitments = value
196 .input_notes
197 .into_iter()
198 .map(|d| {
199 let word: Word = d
200 .nullifier
201 .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
202 .try_into()
203 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
204 Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
205 })
206 .collect::<Result<Vec<_>, RpcError>>()?;
207 let input_notes = InputNotes::new_unchecked(note_commitments);
208
209 let output_note_headers: Vec<NoteHeader> = value
211 .output_notes
212 .into_iter()
213 .map(|proto_header| {
214 proto_header
215 .try_into()
216 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))
217 })
218 .collect::<Result<Vec<_>, RpcError>>()?;
219
220 let mut proof_map: BTreeMap<NoteId, NoteInclusionProof> = BTreeMap::new();
222 for mut proto_proof in output_note_proofs {
223 let note_id: NoteId = proto_proof
224 .note_id
225 .take()
226 .ok_or(RpcError::ExpectedDataMissing("output_note_proofs.note_id".into()))?
227 .try_into()
228 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
229 let inclusion_proof: NoteInclusionProof = proto_proof
230 .try_into()
231 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
232 proof_map.insert(note_id, inclusion_proof);
233 }
234
235 let mut committed_output_notes = Vec::with_capacity(proof_map.len());
237 let mut erased_output_note_ids =
238 Vec::with_capacity(output_note_headers.len().saturating_sub(proof_map.len()));
239
240 for header in &output_note_headers {
241 let note_id = header.id();
242 if let Some(proof) = proof_map.remove(¬e_id) {
243 let metadata = CommittedNoteMetadata::Full(header.metadata().clone());
244 committed_output_notes.push(CommittedNote::new(note_id, metadata, proof));
245 } else {
246 erased_output_note_ids.push(note_id);
247 }
248 }
249
250 let fee_asset: Asset = value
251 .fee
252 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
253 entity: "TransactionHeader",
254 field_name: "fee",
255 })?
256 .try_into()?;
257
258 let fee = match fee_asset {
259 Asset::Fungible(fungible) => fungible,
260 Asset::NonFungible(_) => {
261 return Err(RpcError::InvalidResponse(
262 "expected fungible asset for transaction fee".into(),
263 ));
264 },
265 };
266
267 let transaction_header = TransactionHeader::new(
268 account_id.try_into()?,
269 initial_state_commitment.try_into()?,
270 final_state_commitment.try_into()?,
271 input_notes,
272 output_note_headers,
273 fee,
274 );
275 Ok((transaction_header, committed_output_notes, erased_output_note_ids))
276}