Skip to main content

miden_client/rpc/domain/
transaction.rs

1use 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/// A native asset faucet ID for use in testing scenarios.
20#[cfg(test)]
21pub const ACCOUNT_ID_NATIVE_ASSET_FAUCET: u128 = 0xab00_0000_0000_cd21_0000_ac00_0000_de00_u128;
22
23// INTO TRANSACTION ID
24// ================================================================================================
25
26impl 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// TRANSACTION RECORD
56// ================================================================================================
57
58/// Contains information about a transaction that got included in the chain at a specific block
59/// number.
60#[derive(Debug, Clone)]
61pub struct TransactionRecord {
62    /// Block number in which the transaction was included.
63    pub block_num: BlockNumber,
64    /// A transaction header.
65    pub transaction_header: TransactionHeader,
66    /// Output notes with inclusion proofs, as returned by the node's `SyncTransactions`
67    /// response. Does not include erased notes.
68    pub output_notes: Vec<CommittedNote>,
69    /// Output notes that were erased by same-batch note erasure.
70    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
96/// Converts a proto `TransactionHeader` and its associated output note inclusion proofs
97/// into the domain `TransactionHeader`, committed output notes, and erased note IDs.
98///
99/// The proto `TransactionHeader.output_notes` contains `NoteHeader`s for ALL output notes
100/// (including erased ones). Inclusion proofs for committed notes are provided separately in
101/// `output_note_proofs`. Notes present in `output_notes` but without a corresponding proof
102/// are erased (created and consumed within the same batch).
103fn 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    // Parse all output note headers from the transaction header.
144    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    // Build a map of note_id to inclusion_proof from the separate proofs field.
155    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    // Join: notes with a matching proof are committed; notes without are erased.
170    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(&note_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}