1use alloc::{
4 collections::{BTreeMap, BTreeSet},
5 string::{String, ToString},
6 vec::Vec,
7};
8
9use miden_lib::account::interface::{AccountInterface, AccountInterfaceError};
10use miden_objects::{
11 Digest, Felt, NoteError, Word,
12 account::AccountId,
13 assembly::AssemblyError,
14 crypto::merkle::MerkleStore,
15 note::{Note, NoteDetails, NoteId, NoteTag, PartialNote},
16 transaction::{TransactionArgs, TransactionScript},
17 vm::AdviceMap,
18};
19use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
20use thiserror::Error;
21
22mod builder;
23pub use builder::{PaymentTransactionData, SwapTransactionData, TransactionRequestBuilder};
24
25mod foreign;
26pub use foreign::{ForeignAccount, ForeignAccountInputs};
27
28pub type NoteArgs = Word;
32
33#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum TransactionScriptTemplate {
39 CustomScript(TransactionScript),
41 SendNotes(Vec<PartialNote>),
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct TransactionRequest {
56 unauthenticated_input_notes: Vec<Note>,
58 input_notes: BTreeMap<NoteId, Option<NoteArgs>>,
61 script_template: Option<TransactionScriptTemplate>,
63 expected_output_notes: BTreeMap<NoteId, Note>,
65 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
70 advice_map: AdviceMap,
72 merkle_store: MerkleStore,
74 foreign_accounts: BTreeSet<ForeignAccount>,
78 expiration_delta: Option<u16>,
81}
82
83impl TransactionRequest {
84 pub fn unauthenticated_input_notes(&self) -> &[Note] {
89 &self.unauthenticated_input_notes
90 }
91
92 pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
94 self.unauthenticated_input_notes.iter().map(Note::id)
95 }
96
97 pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
99 let unauthenticated_note_ids =
100 self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
101
102 self.input_notes()
103 .iter()
104 .map(|(note_id, _)| *note_id)
105 .filter(move |note_id| !unauthenticated_note_ids.contains(note_id))
106 }
107
108 pub fn input_notes(&self) -> &BTreeMap<NoteId, Option<NoteArgs>> {
110 &self.input_notes
111 }
112
113 pub fn get_input_note_ids(&self) -> Vec<NoteId> {
115 self.input_notes.keys().copied().collect()
116 }
117
118 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
121 self.input_notes
122 .iter()
123 .filter_map(|(note, args)| args.map(|a| (*note, a)))
124 .collect()
125 }
126
127 pub fn expected_output_notes(&self) -> impl Iterator<Item = &Note> {
129 self.expected_output_notes.values()
130 }
131
132 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
134 self.expected_future_notes.values()
135 }
136
137 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
139 &self.script_template
140 }
141
142 pub fn advice_map(&self) -> &AdviceMap {
144 &self.advice_map
145 }
146
147 pub fn merkle_store(&self) -> &MerkleStore {
149 &self.merkle_store
150 }
151
152 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
154 &self.foreign_accounts
155 }
156
157 pub(super) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
160 let note_args = self.get_note_args();
161 let TransactionRequest {
162 expected_output_notes,
163 advice_map,
164 merkle_store,
165 ..
166 } = self;
167
168 let mut tx_args = TransactionArgs::new(Some(tx_script), note_args.into(), advice_map);
169
170 tx_args.extend_expected_output_notes(expected_output_notes.into_values());
171 tx_args.extend_merkle_store(merkle_store.inner_nodes());
172
173 tx_args
174 }
175
176 pub(crate) fn build_transaction_script(
179 &self,
180 account_interface: &AccountInterface,
181 in_debug_mode: bool,
182 ) -> Result<TransactionScript, TransactionRequestError> {
183 match &self.script_template {
184 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
185 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
186 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode)?),
187 None => {
188 if self.input_notes.is_empty() {
189 Err(TransactionRequestError::NoInputNotes)
190 } else {
191 Ok(account_interface.build_auth_script(in_debug_mode)?)
192 }
193 },
194 }
195 }
196}
197
198impl Serializable for TransactionRequest {
202 fn write_into<W: ByteWriter>(&self, target: &mut W) {
203 self.unauthenticated_input_notes.write_into(target);
204 self.input_notes.write_into(target);
205 match &self.script_template {
206 None => target.write_u8(0),
207 Some(TransactionScriptTemplate::CustomScript(script)) => {
208 target.write_u8(1);
209 script.write_into(target);
210 },
211 Some(TransactionScriptTemplate::SendNotes(notes)) => {
212 target.write_u8(2);
213 notes.write_into(target);
214 },
215 }
216 self.expected_output_notes.write_into(target);
217 self.expected_future_notes.write_into(target);
218 self.advice_map.clone().into_iter().collect::<Vec<_>>().write_into(target);
219 self.merkle_store.write_into(target);
220 self.foreign_accounts.write_into(target);
221 self.expiration_delta.write_into(target);
222 }
223}
224
225impl Deserializable for TransactionRequest {
226 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
227 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
228 let input_notes = BTreeMap::<NoteId, Option<NoteArgs>>::read_from(source)?;
229
230 let script_template = match source.read_u8()? {
231 0 => None,
232 1 => {
233 let transaction_script = TransactionScript::read_from(source)?;
234 Some(TransactionScriptTemplate::CustomScript(transaction_script))
235 },
236 2 => {
237 let notes = Vec::<PartialNote>::read_from(source)?;
238 Some(TransactionScriptTemplate::SendNotes(notes))
239 },
240 _ => {
241 return Err(DeserializationError::InvalidValue(
242 "Invalid script template type".to_string(),
243 ));
244 },
245 };
246
247 let expected_output_notes = BTreeMap::<NoteId, Note>::read_from(source)?;
248 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
249
250 let mut advice_map = AdviceMap::new();
251 let advice_vec = Vec::<(Digest, Vec<Felt>)>::read_from(source)?;
252 advice_map.extend(advice_vec);
253 let merkle_store = MerkleStore::read_from(source)?;
254 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
255 let expiration_delta = Option::<u16>::read_from(source)?;
256
257 Ok(TransactionRequest {
258 unauthenticated_input_notes,
259 input_notes,
260 script_template,
261 expected_output_notes,
262 expected_future_notes,
263 advice_map,
264 merkle_store,
265 foreign_accounts,
266 expiration_delta,
267 })
268 }
269}
270
271impl Default for TransactionRequestBuilder {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277#[derive(Debug, Error)]
282pub enum TransactionRequestError {
283 #[error("foreign account data missing in the account proof")]
284 ForeignAccountDataMissing,
285 #[error("foreign account storage slot {0} is not a map type")]
286 ForeignAccountStorageSlotInvalidIndex(u8),
287 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
288 InvalidForeignAccountId(AccountId),
289 #[error(
290 "every authenticated note to be consumed should be committed and contain a valid inclusion proof"
291 )]
292 InputNoteNotAuthenticated,
293 #[error("the input notes map should include keys for all provided unauthenticated input notes")]
294 InputNotesMapMissingUnauthenticatedNotes,
295 #[error("own notes shouldn't be of the header variant")]
296 InvalidNoteVariant,
297 #[error("invalid sender account id: {0}")]
298 InvalidSenderAccount(AccountId),
299 #[error("invalid transaction script")]
300 InvalidTransactionScript(#[from] AssemblyError),
301 #[error("a transaction without output notes must have at least one input note")]
302 NoInputNotes,
303 #[error("note not found: {0}")]
304 NoteNotFound(String),
305 #[error("note creation error")]
306 NoteCreationError(#[from] NoteError),
307 #[error("pay to id note doesn't contain at least one asset")]
308 P2IDNoteWithoutAsset,
309 #[error("transaction script template error: {0}")]
310 ScriptTemplateError(String),
311 #[error("storage slot {0} not found in account ID {1}")]
312 StorageSlotNotFound(u8, AccountId),
313 #[error("account interface error")]
314 AccountInterfaceError(#[from] AccountInterfaceError),
315}
316
317#[cfg(test)]
321mod tests {
322 use std::vec::Vec;
323
324 use miden_lib::{note::create_p2id_note, transaction::TransactionKernel};
325 use miden_objects::{
326 Digest, Felt, ZERO,
327 account::{AccountBuilder, AccountId, AccountIdAnchor, AccountType},
328 asset::FungibleAsset,
329 crypto::rand::{FeltRng, RpoRandomCoin},
330 note::{NoteExecutionMode, NoteTag, NoteType},
331 testing::{
332 account_component::AccountMockComponent,
333 account_id::{
334 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
335 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER,
336 },
337 },
338 transaction::OutputNote,
339 };
340 use miden_tx::utils::{Deserializable, Serializable};
341
342 use super::{TransactionRequest, TransactionRequestBuilder};
343 use crate::{
344 rpc::domain::account::AccountStorageRequirements,
345 transaction::{ForeignAccount, ForeignAccountInputs},
346 };
347
348 #[test]
349 fn transaction_request_serialization() {
350 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
351 let target_id =
352 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
353 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
354 let mut rng = RpoRandomCoin::new(Default::default());
355
356 let mut notes = vec![];
357 for i in 0..6 {
358 let note = create_p2id_note(
359 sender_id,
360 target_id,
361 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
362 NoteType::Private,
363 ZERO,
364 &mut rng,
365 )
366 .unwrap();
367 notes.push(note);
368 }
369
370 let mut advice_vec: Vec<(Digest, Vec<Felt>)> = vec![];
371 for i in 0..10 {
372 advice_vec.push((Digest::new(rng.draw_word()), vec![Felt::new(i)]));
373 }
374
375 let account = AccountBuilder::new(Default::default())
376 .anchor(AccountIdAnchor::new_unchecked(0, Digest::default()))
377 .with_component(
378 AccountMockComponent::new_with_empty_slots(TransactionKernel::assembler()).unwrap(),
379 )
380 .account_type(AccountType::RegularAccountImmutableCode)
381 .storage_mode(miden_objects::account::AccountStorageMode::Private)
382 .build_existing()
383 .unwrap();
384
385 let tx_request = TransactionRequestBuilder::new()
387 .with_authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
388 .with_unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
389 .with_expected_output_notes(vec![notes.pop().unwrap()])
390 .with_expected_future_notes(vec![(
391 notes.pop().unwrap().into(),
392 NoteTag::from_account_id(sender_id, NoteExecutionMode::Local).unwrap(),
393 )])
394 .extend_advice_map(advice_vec)
395 .with_foreign_accounts([
396 ForeignAccount::public(
397 target_id,
398 AccountStorageRequirements::new([(5u8, &[Digest::default()])]),
399 )
400 .unwrap(),
401 ForeignAccount::private(
402 ForeignAccountInputs::from_account(
403 account,
404 &AccountStorageRequirements::default(),
405 )
406 .unwrap(),
407 )
408 .unwrap(),
409 ])
410 .with_own_output_notes(vec![
411 OutputNote::Full(notes.pop().unwrap()),
412 OutputNote::Partial(notes.pop().unwrap().into()),
413 ])
414 .build()
415 .unwrap();
416
417 let mut buffer = Vec::new();
418 tx_request.write_into(&mut buffer);
419
420 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
421 assert_eq!(tx_request, deserialized_tx_request);
422 }
423}