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::{AccountInputs, 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;
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 ignore_invalid_input_notes: bool,
85}
86
87impl TransactionRequest {
88 pub fn unauthenticated_input_notes(&self) -> &[Note] {
93 &self.unauthenticated_input_notes
94 }
95
96 pub fn unauthenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
98 self.unauthenticated_input_notes.iter().map(Note::id)
99 }
100
101 pub fn authenticated_input_note_ids(&self) -> impl Iterator<Item = NoteId> + '_ {
103 let unauthenticated_note_ids =
104 self.unauthenticated_input_note_ids().collect::<BTreeSet<_>>();
105
106 self.input_notes()
107 .keys()
108 .copied()
109 .filter(move |note_id| !unauthenticated_note_ids.contains(note_id))
110 }
111
112 pub fn input_notes(&self) -> &BTreeMap<NoteId, Option<NoteArgs>> {
114 &self.input_notes
115 }
116
117 pub fn get_input_note_ids(&self) -> Vec<NoteId> {
119 self.input_notes.keys().copied().collect()
120 }
121
122 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
125 self.input_notes
126 .iter()
127 .filter_map(|(note, args)| args.map(|a| (*note, a)))
128 .collect()
129 }
130
131 pub fn expected_output_notes(&self) -> impl Iterator<Item = &Note> {
133 self.expected_output_notes.values()
134 }
135
136 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
138 self.expected_future_notes.values()
139 }
140
141 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
143 &self.script_template
144 }
145
146 pub fn advice_map(&self) -> &AdviceMap {
148 &self.advice_map
149 }
150
151 pub fn merkle_store(&self) -> &MerkleStore {
153 &self.merkle_store
154 }
155
156 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
158 &self.foreign_accounts
159 }
160
161 pub fn ignore_invalid_input_notes(&self) -> bool {
163 self.ignore_invalid_input_notes
164 }
165
166 pub(crate) fn into_transaction_args(
169 self,
170 tx_script: TransactionScript,
171 foreign_account_inputs: Vec<AccountInputs>,
172 ) -> TransactionArgs {
173 let note_args = self.get_note_args();
174 let TransactionRequest {
175 expected_output_notes,
176 advice_map,
177 merkle_store,
178 ..
179 } = self;
180
181 let mut tx_args = TransactionArgs::new(
182 Some(tx_script),
183 note_args.into(),
184 advice_map,
185 foreign_account_inputs,
186 );
187
188 tx_args.extend_output_note_recipients(expected_output_notes.into_values());
189 tx_args.extend_merkle_store(merkle_store.inner_nodes());
190
191 tx_args
192 }
193
194 pub(crate) fn build_transaction_script(
197 &self,
198 account_interface: &AccountInterface,
199 in_debug_mode: bool,
200 ) -> Result<TransactionScript, TransactionRequestError> {
201 match &self.script_template {
202 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
203 Some(TransactionScriptTemplate::SendNotes(notes)) => Ok(account_interface
204 .build_send_notes_script(notes, self.expiration_delta, in_debug_mode)?),
205 None => {
206 if self.input_notes.is_empty() {
207 Err(TransactionRequestError::NoInputNotes)
208 } else {
209 Ok(account_interface.build_auth_script(in_debug_mode)?)
210 }
211 },
212 }
213 }
214}
215
216impl Serializable for TransactionRequest {
220 fn write_into<W: ByteWriter>(&self, target: &mut W) {
221 self.unauthenticated_input_notes.write_into(target);
222 self.input_notes.write_into(target);
223 match &self.script_template {
224 None => target.write_u8(0),
225 Some(TransactionScriptTemplate::CustomScript(script)) => {
226 target.write_u8(1);
227 script.write_into(target);
228 },
229 Some(TransactionScriptTemplate::SendNotes(notes)) => {
230 target.write_u8(2);
231 notes.write_into(target);
232 },
233 }
234 self.expected_output_notes.write_into(target);
235 self.expected_future_notes.write_into(target);
236 self.advice_map.clone().into_iter().collect::<Vec<_>>().write_into(target);
237 self.merkle_store.write_into(target);
238 self.foreign_accounts.write_into(target);
239 self.expiration_delta.write_into(target);
240 target.write_u8(u8::from(self.ignore_invalid_input_notes));
241 }
242}
243
244impl Deserializable for TransactionRequest {
245 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
246 let unauthenticated_input_notes = Vec::<Note>::read_from(source)?;
247 let input_notes = BTreeMap::<NoteId, Option<NoteArgs>>::read_from(source)?;
248
249 let script_template = match source.read_u8()? {
250 0 => None,
251 1 => {
252 let transaction_script = TransactionScript::read_from(source)?;
253 Some(TransactionScriptTemplate::CustomScript(transaction_script))
254 },
255 2 => {
256 let notes = Vec::<PartialNote>::read_from(source)?;
257 Some(TransactionScriptTemplate::SendNotes(notes))
258 },
259 _ => {
260 return Err(DeserializationError::InvalidValue(
261 "Invalid script template type".to_string(),
262 ));
263 },
264 };
265
266 let expected_output_notes = BTreeMap::<NoteId, Note>::read_from(source)?;
267 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
268
269 let mut advice_map = AdviceMap::new();
270 let advice_vec = Vec::<(Digest, Vec<Felt>)>::read_from(source)?;
271 advice_map.extend(advice_vec);
272 let merkle_store = MerkleStore::read_from(source)?;
273 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
274 let expiration_delta = Option::<u16>::read_from(source)?;
275 let ignore_invalid_input_notes = source.read_u8()? == 1;
276
277 Ok(TransactionRequest {
278 unauthenticated_input_notes,
279 input_notes,
280 script_template,
281 expected_output_notes,
282 expected_future_notes,
283 advice_map,
284 merkle_store,
285 foreign_accounts,
286 expiration_delta,
287 ignore_invalid_input_notes,
288 })
289 }
290}
291
292impl Default for TransactionRequestBuilder {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298#[derive(Debug, Error)]
303pub enum TransactionRequestError {
304 #[error("foreign account data missing in the account proof")]
305 ForeignAccountDataMissing,
306 #[error("foreign account storage slot {0} is not a map type")]
307 ForeignAccountStorageSlotInvalidIndex(u8),
308 #[error("requested foreign account with ID {0} does not have an expected storage mode")]
309 InvalidForeignAccountId(AccountId),
310 #[error("note {0} does not contain a valid inclusion proof")]
311 InputNoteNotAuthenticated(NoteId),
312 #[error("note {0} has already been consumed")]
313 InputNoteAlreadyConsumed(NoteId),
314 #[error("the input notes map should include keys for all provided unauthenticated input notes")]
315 InputNotesMapMissingUnauthenticatedNotes,
316 #[error("own notes shouldn't be of the header variant")]
317 InvalidNoteVariant,
318 #[error("invalid sender account id: {0}")]
319 InvalidSenderAccount(AccountId),
320 #[error("invalid transaction script")]
321 InvalidTransactionScript(#[from] AssemblyError),
322 #[error("a transaction without output notes must have at least one input note")]
323 NoInputNotes,
324 #[error("note not found: {0}")]
325 NoteNotFound(String),
326 #[error("note creation error")]
327 NoteCreationError(#[from] NoteError),
328 #[error("pay to id note doesn't contain at least one asset")]
329 P2IDNoteWithoutAsset,
330 #[error("transaction script template error: {0}")]
331 ScriptTemplateError(String),
332 #[error("storage slot {0} not found in account ID {1}")]
333 StorageSlotNotFound(u8, AccountId),
334 #[error("account interface error")]
335 AccountInterfaceError(#[from] AccountInterfaceError),
336}
337
338#[cfg(test)]
342mod tests {
343 use std::vec::Vec;
344
345 use miden_lib::{note::create_p2id_note, transaction::TransactionKernel};
346 use miden_objects::{
347 Digest, Felt, ZERO,
348 account::{AccountBuilder, AccountId, AccountIdAnchor, AccountType},
349 asset::FungibleAsset,
350 crypto::rand::{FeltRng, RpoRandomCoin},
351 note::{NoteExecutionMode, NoteTag, NoteType},
352 testing::{
353 account_component::AccountMockComponent,
354 account_id::{
355 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
356 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE, ACCOUNT_ID_SENDER,
357 },
358 },
359 transaction::OutputNote,
360 };
361 use miden_tx::utils::{Deserializable, Serializable};
362
363 use super::{TransactionRequest, TransactionRequestBuilder};
364 use crate::{rpc::domain::account::AccountStorageRequirements, transaction::ForeignAccount};
365
366 #[test]
367 fn transaction_request_serialization() {
368 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
369 let target_id =
370 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
371 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
372 let mut rng = RpoRandomCoin::new(Default::default());
373
374 let mut notes = vec![];
375 for i in 0..6 {
376 let note = create_p2id_note(
377 sender_id,
378 target_id,
379 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
380 NoteType::Private,
381 ZERO,
382 &mut rng,
383 )
384 .unwrap();
385 notes.push(note);
386 }
387
388 let mut advice_vec: Vec<(Digest, Vec<Felt>)> = vec![];
389 for i in 0..10 {
390 advice_vec.push((Digest::new(rng.draw_word()), vec![Felt::new(i)]));
391 }
392
393 let account = AccountBuilder::new(Default::default())
394 .anchor(AccountIdAnchor::new_unchecked(0, Digest::default()))
395 .with_component(
396 AccountMockComponent::new_with_empty_slots(TransactionKernel::assembler()).unwrap(),
397 )
398 .account_type(AccountType::RegularAccountImmutableCode)
399 .storage_mode(miden_objects::account::AccountStorageMode::Private)
400 .build_existing()
401 .unwrap();
402
403 let tx_request = TransactionRequestBuilder::new()
405 .with_authenticated_input_notes(vec![(notes.pop().unwrap().id(), None)])
406 .with_unauthenticated_input_notes(vec![(notes.pop().unwrap(), None)])
407 .with_expected_output_notes(vec![notes.pop().unwrap()])
408 .with_expected_future_notes(vec![(
409 notes.pop().unwrap().into(),
410 NoteTag::from_account_id(sender_id, NoteExecutionMode::Local).unwrap(),
411 )])
412 .extend_advice_map(advice_vec)
413 .with_foreign_accounts([
414 ForeignAccount::public(
415 target_id,
416 AccountStorageRequirements::new([(5u8, &[Digest::default()])]),
417 )
418 .unwrap(),
419 ForeignAccount::private(account).unwrap(),
420 ])
421 .with_own_output_notes(vec![
422 OutputNote::Full(notes.pop().unwrap()),
423 OutputNote::Partial(notes.pop().unwrap().into()),
424 ])
425 .build()
426 .unwrap();
427
428 let mut buffer = Vec::new();
429 tx_request.write_into(&mut buffer);
430
431 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
432 assert_eq!(tx_request, deserialized_tx_request);
433 }
434}