miden_client/rpc/domain/
transaction.rs1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::AccountId;
6use miden_protocol::asset::Asset;
7use miden_protocol::block::BlockNumber;
8use miden_protocol::note::{NoteHeader, 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_cd20_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)]
60pub struct TransactionInclusion {
61 pub transaction_id: TransactionId,
63 pub block_num: BlockNumber,
65 pub account_id: AccountId,
67 pub initial_state_commitment: Word,
69 pub nullifiers: Vec<Nullifier>,
71 pub output_notes: Vec<CommittedNote>,
73}
74
75#[derive(Debug, Clone)]
80pub struct TransactionsInfo {
81 pub chain_tip: BlockNumber,
83 pub block_num: BlockNumber,
85 pub transaction_records: Vec<TransactionRecord>,
87}
88
89impl TryFrom<proto::rpc::SyncTransactionsResponse> for TransactionsInfo {
90 type Error = RpcError;
91
92 fn try_from(value: proto::rpc::SyncTransactionsResponse) -> Result<Self, Self::Error> {
93 let pagination_info = value.pagination_info.ok_or(
94 RpcConversionError::MissingFieldInProtobufRepresentation {
95 entity: "SyncTransactionsResponse",
96 field_name: "pagination_info",
97 },
98 )?;
99
100 let chain_tip = pagination_info.chain_tip.into();
101 let block_num = pagination_info.block_num.into();
102
103 let transaction_records = value
104 .transactions
105 .into_iter()
106 .map(TryInto::try_into)
107 .collect::<Result<Vec<TransactionRecord>, RpcError>>()?;
108
109 Ok(Self {
110 chain_tip,
111 block_num,
112 transaction_records,
113 })
114 }
115}
116
117#[derive(Debug, Clone)]
123pub struct TransactionRecord {
124 pub block_num: BlockNumber,
126 pub transaction_header: TransactionHeader,
128 pub output_notes: Vec<CommittedNote>,
131}
132
133impl TryFrom<proto::rpc::TransactionRecord> for TransactionRecord {
134 type Error = RpcError;
135
136 fn try_from(value: proto::rpc::TransactionRecord) -> Result<Self, Self::Error> {
137 let block_num = value.block_num.into();
138 let proto_header =
139 value.header.ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
140 entity: "TransactionRecord",
141 field_name: "transaction_header",
142 })?;
143
144 let (transaction_header, output_notes) = convert_transaction_header(proto_header)?;
145
146 Ok(Self {
147 block_num,
148 transaction_header,
149 output_notes,
150 })
151 }
152}
153
154fn convert_transaction_header(
162 value: proto::transaction::TransactionHeader,
163) -> Result<(TransactionHeader, Vec<CommittedNote>), RpcError> {
164 let account_id =
165 value
166 .account_id
167 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
168 entity: "TransactionHeader",
169 field_name: "account_id",
170 })?;
171
172 let initial_state_commitment = value.initial_state_commitment.ok_or(
173 RpcConversionError::MissingFieldInProtobufRepresentation {
174 entity: "TransactionHeader",
175 field_name: "initial_state_commitment",
176 },
177 )?;
178
179 let final_state_commitment = value.final_state_commitment.ok_or(
180 RpcConversionError::MissingFieldInProtobufRepresentation {
181 entity: "TransactionHeader",
182 field_name: "final_state_commitment",
183 },
184 )?;
185
186 let note_commitments = value
187 .input_notes
188 .into_iter()
189 .map(|d| {
190 let word: Word = d
191 .nullifier
192 .ok_or(RpcError::ExpectedDataMissing("nullifier".into()))?
193 .try_into()
194 .map_err(|e: RpcConversionError| RpcError::InvalidResponse(e.to_string()))?;
195 Ok(InputNoteCommitment::from(Nullifier::from_raw(word)))
196 })
197 .collect::<Result<Vec<_>, RpcError>>()?;
198 let input_notes = InputNotes::new_unchecked(note_commitments);
199
200 let mut committed_output_notes = Vec::with_capacity(value.output_notes.len());
205 let mut output_note_headers = Vec::with_capacity(value.output_notes.len());
206
207 for record in value.output_notes {
208 let note = CommittedNote::try_from(record).map_err(RpcError::from)?;
209 if let Some(metadata) = note.metadata() {
210 output_note_headers.push(NoteHeader::new(*note.note_id(), metadata.clone()));
211 }
212 committed_output_notes.push(note);
213 }
214
215 let fee_asset: Asset = value
216 .fee
217 .ok_or(RpcConversionError::MissingFieldInProtobufRepresentation {
218 entity: "TransactionHeader",
219 field_name: "fee",
220 })?
221 .try_into()?;
222
223 let fee = match fee_asset {
224 Asset::Fungible(fungible) => fungible,
225 Asset::NonFungible(_) => {
226 return Err(RpcError::InvalidResponse(
227 "expected fungible asset for transaction fee".into(),
228 ));
229 },
230 };
231
232 let transaction_header = TransactionHeader::new(
233 account_id.try_into()?,
234 initial_state_commitment.try_into()?,
235 final_state_commitment.try_into()?,
236 input_notes,
237 output_note_headers,
238 fee,
239 );
240 Ok((transaction_header, committed_output_notes))
241}