1use alloc::boxed::Box;
4use alloc::collections::{BTreeMap, BTreeSet};
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use miden_protocol::Word;
9use miden_protocol::account::AccountId;
10use miden_protocol::asset::{Asset, NonFungibleAsset};
11use miden_protocol::crypto::merkle::MerkleError;
12use miden_protocol::crypto::merkle::store::MerkleStore;
13use miden_protocol::errors::{
14 AccountError,
15 AssetVaultError,
16 NoteError,
17 StorageMapError,
18 TransactionInputError,
19 TransactionScriptError,
20};
21use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteTag, PartialNote};
22use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
23use miden_protocol::vm::AdviceMap;
24use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
25use miden_standards::code_builder::CodeBuilder;
26use miden_standards::errors::CodeBuilderError;
27use miden_tx::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
28use thiserror::Error;
29
30mod builder;
31pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
32
33mod foreign;
34pub use foreign::{ForeignAccount, account_proof_into_inputs};
35
36use crate::store::InputNoteRecord;
37
38pub type NoteArgs = Word;
42
43#[derive(Clone, Debug, PartialEq, Eq)]
48pub enum TransactionScriptTemplate {
49 CustomScript(TransactionScript),
51 SendNotes(Vec<PartialNote>),
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct TransactionRequest {
66 input_notes: Vec<Note>,
71 input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
74 script_template: Option<TransactionScriptTemplate>,
76 expected_output_recipients: BTreeMap<Word, NoteRecipient>,
78 expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
83 advice_map: AdviceMap,
85 merkle_store: MerkleStore,
87 foreign_accounts: BTreeSet<ForeignAccount>,
91 expiration_delta: Option<u16>,
94 ignore_invalid_input_notes: bool,
98 script_arg: Option<Word>,
101 auth_arg: Option<Word>,
104}
105
106impl TransactionRequest {
107 pub fn input_notes(&self) -> &[Note] {
112 &self.input_notes
113 }
114
115 pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
117 self.input_notes.iter().map(Note::id)
118 }
119
120 pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
122 collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
123 }
124
125 pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
128 self.input_notes_args
129 .iter()
130 .filter_map(|(note, args)| args.map(|a| (*note, a)))
131 .collect()
132 }
133
134 pub fn expected_output_own_notes(&self) -> Vec<Note> {
140 match &self.script_template {
141 Some(TransactionScriptTemplate::SendNotes(notes)) => notes
142 .iter()
143 .map(|partial| {
144 Note::new(
145 partial.assets().clone(),
146 partial.metadata().clone(),
147 self.expected_output_recipients
148 .get(&partial.recipient_digest())
149 .expect("Recipient should be included if it's an own note")
150 .clone(),
151 )
152 })
153 .collect(),
154 _ => vec![],
155 }
156 }
157
158 pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
160 self.expected_output_recipients.values()
161 }
162
163 pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
165 self.expected_future_notes.values()
166 }
167
168 pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
170 &self.script_template
171 }
172
173 pub fn advice_map(&self) -> &AdviceMap {
175 &self.advice_map
176 }
177
178 pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
180 &mut self.advice_map
181 }
182
183 pub fn merkle_store(&self) -> &MerkleStore {
185 &self.merkle_store
186 }
187
188 pub fn foreign_accounts(&self) -> &BTreeSet<ForeignAccount> {
190 &self.foreign_accounts
191 }
192
193 pub fn ignore_invalid_input_notes(&self) -> bool {
195 self.ignore_invalid_input_notes
196 }
197
198 pub fn script_arg(&self) -> &Option<Word> {
200 &self.script_arg
201 }
202
203 pub fn auth_arg(&self) -> &Option<Word> {
205 &self.auth_arg
206 }
207
208 pub(crate) fn build_input_notes(
215 &self,
216 authenticated_note_records: Vec<InputNoteRecord>,
217 ) -> Result<InputNotes<InputNote>, TransactionRequestError> {
218 let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
219
220 for authenticated_note_record in authenticated_note_records {
222 if !authenticated_note_record.is_authenticated() {
223 return Err(TransactionRequestError::InputNoteNotAuthenticated(
224 authenticated_note_record.id(),
225 ));
226 }
227
228 if authenticated_note_record.is_consumed() {
229 return Err(TransactionRequestError::InputNoteAlreadyConsumed(
230 authenticated_note_record.id(),
231 ));
232 }
233
234 let authenticated_note_id = authenticated_note_record.id();
235 input_notes.insert(
236 authenticated_note_id,
237 authenticated_note_record
238 .try_into()
239 .expect("Authenticated note record should be convertible to InputNote"),
240 );
241 }
242
243 let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
245 for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
246 {
247 input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
248 }
249
250 Ok(InputNotes::new(
251 self.input_note_ids()
252 .map(|note_id| {
253 input_notes
254 .remove(¬e_id)
255 .expect("The input note map was checked to contain all input notes")
256 })
257 .collect(),
258 )?)
259 }
260
261 pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
264 let note_args = self.get_note_args();
265 let TransactionRequest {
266 expected_output_recipients,
267 advice_map,
268 merkle_store,
269 ..
270 } = self;
271
272 let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
273
274 tx_args = if let Some(argument) = self.script_arg {
275 tx_args.with_tx_script_and_args(tx_script, argument)
276 } else {
277 tx_args.with_tx_script(tx_script)
278 };
279
280 if let Some(auth_argument) = self.auth_arg {
281 tx_args = tx_args.with_auth_args(auth_argument);
282 }
283
284 tx_args
285 .extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
286 tx_args.extend_merkle_store(merkle_store.inner_nodes());
287
288 tx_args
289 }
290
291 pub(crate) fn build_transaction_script(
294 &self,
295 account_interface: &AccountInterface,
296 ) -> Result<TransactionScript, TransactionRequestError> {
297 match &self.script_template {
298 Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
299 Some(TransactionScriptTemplate::SendNotes(notes)) => {
300 Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
301 },
302 None => {
303 let empty_script = CodeBuilder::new().compile_tx_script("begin nop end")?;
304
305 Ok(empty_script)
306 },
307 }
308 }
309}
310
311impl Serializable for TransactionRequest {
315 fn write_into<W: ByteWriter>(&self, target: &mut W) {
316 self.input_notes.write_into(target);
317 self.input_notes_args.write_into(target);
318 match &self.script_template {
319 None => target.write_u8(0),
320 Some(TransactionScriptTemplate::CustomScript(script)) => {
321 target.write_u8(1);
322 script.write_into(target);
323 },
324 Some(TransactionScriptTemplate::SendNotes(notes)) => {
325 target.write_u8(2);
326 notes.write_into(target);
327 },
328 }
329 self.expected_output_recipients.write_into(target);
330 self.expected_future_notes.write_into(target);
331 self.advice_map.write_into(target);
332 self.merkle_store.write_into(target);
333 self.foreign_accounts.write_into(target);
334 self.expiration_delta.write_into(target);
335 target.write_u8(u8::from(self.ignore_invalid_input_notes));
336 self.script_arg.write_into(target);
337 self.auth_arg.write_into(target);
338 }
339}
340
341impl Deserializable for TransactionRequest {
342 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
343 let input_notes = Vec::<Note>::read_from(source)?;
344 let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
345
346 let script_template = match source.read_u8()? {
347 0 => None,
348 1 => {
349 let transaction_script = TransactionScript::read_from(source)?;
350 Some(TransactionScriptTemplate::CustomScript(transaction_script))
351 },
352 2 => {
353 let notes = Vec::<PartialNote>::read_from(source)?;
354 Some(TransactionScriptTemplate::SendNotes(notes))
355 },
356 _ => {
357 return Err(DeserializationError::InvalidValue(
358 "Invalid script template type".to_string(),
359 ));
360 },
361 };
362
363 let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
364 let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
365
366 let advice_map = AdviceMap::read_from(source)?;
367 let merkle_store = MerkleStore::read_from(source)?;
368 let foreign_accounts = BTreeSet::<ForeignAccount>::read_from(source)?;
369 let expiration_delta = Option::<u16>::read_from(source)?;
370 let ignore_invalid_input_notes = source.read_u8()? == 1;
371 let script_arg = Option::<Word>::read_from(source)?;
372 let auth_arg = Option::<Word>::read_from(source)?;
373
374 Ok(TransactionRequest {
375 input_notes,
376 input_notes_args,
377 script_template,
378 expected_output_recipients,
379 expected_future_notes,
380 advice_map,
381 merkle_store,
382 foreign_accounts,
383 expiration_delta,
384 ignore_invalid_input_notes,
385 script_arg,
386 auth_arg,
387 })
388 }
389}
390
391pub(crate) fn collect_assets<'a>(
396 assets: impl Iterator<Item = &'a Asset>,
397) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
398 let mut fungible_balance_map = BTreeMap::new();
399 let mut non_fungible_set = BTreeSet::new();
400
401 assets.for_each(|asset| match asset {
402 Asset::Fungible(fungible) => {
403 fungible_balance_map
404 .entry(fungible.faucet_id())
405 .and_modify(|balance| *balance += fungible.amount())
406 .or_insert(fungible.amount());
407 },
408 Asset::NonFungible(non_fungible) => {
409 non_fungible_set.insert(*non_fungible);
410 },
411 });
412
413 (fungible_balance_map, non_fungible_set)
414}
415
416impl Default for TransactionRequestBuilder {
417 fn default() -> Self {
418 Self::new()
419 }
420}
421
422#[derive(Debug, Error)]
427pub enum TransactionRequestError {
428 #[error("account interface error")]
429 AccountInterfaceError(#[from] AccountInterfaceError),
430 #[error("account error")]
431 AccountError(#[from] AccountError),
432 #[error("duplicate input note: note {0} was added more than once to the transaction")]
433 DuplicateInputNote(NoteId),
434 #[error(
435 "the account proof does not contain the required foreign account data; re-fetch the proof and retry"
436 )]
437 ForeignAccountDataMissing,
438 #[error(
439 "foreign account {0} has an incompatible storage mode; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
440 )]
441 InvalidForeignAccountId(AccountId),
442 #[error(
443 "note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
444 )]
445 InputNoteNotAuthenticated(NoteId),
446 #[error("note {0} has already been consumed")]
447 InputNoteAlreadyConsumed(NoteId),
448 #[error("internal error: own notes must contain full note data, not just a header")]
449 InvalidNoteVariant,
450 #[error("sender account {0} is not tracked by this client or does not exist")]
451 InvalidSenderAccount(AccountId),
452 #[error("invalid transaction script")]
453 InvalidTransactionScript(#[from] TransactionScriptError),
454 #[error("merkle proof error")]
455 MerkleError(#[from] MerkleError),
456 #[error("empty transaction: the request has no input notes and no account state changes")]
457 NoInputNotesNorAccountChange,
458 #[error("note not found: {0}")]
459 NoteNotFound(String),
460 #[error("failed to create note")]
461 NoteCreationError(#[from] NoteError),
462 #[error("pay-to-ID note must contain at least one asset to transfer")]
463 P2IDNoteWithoutAsset,
464 #[error("error building script")]
465 CodeBuilderError(#[from] CodeBuilderError),
466 #[error("transaction script template error: {0}")]
467 ScriptTemplateError(String),
468 #[error("storage slot {0} not found in account ID {1}")]
469 StorageSlotNotFound(u8, AccountId),
470 #[error("error while building the input notes")]
471 TransactionInputError(#[from] TransactionInputError),
472 #[error("account storage map error")]
473 StorageMapError(#[from] StorageMapError),
474 #[error("asset vault error")]
475 AssetVaultError(#[from] AssetVaultError),
476 #[error(
477 "unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
478 )]
479 UnsupportedAuthSchemeId(u8),
480}
481
482#[cfg(test)]
486mod tests {
487 use std::vec::Vec;
488
489 use miden_protocol::account::auth::PublicKeyCommitment;
490 use miden_protocol::account::{
491 AccountBuilder,
492 AccountComponent,
493 AccountId,
494 AccountType,
495 StorageSlotName,
496 };
497 use miden_protocol::asset::FungibleAsset;
498 use miden_protocol::crypto::rand::{FeltRng, RpoRandomCoin};
499 use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
500 use miden_protocol::testing::account_id::{
501 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
502 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
503 ACCOUNT_ID_SENDER,
504 };
505 use miden_protocol::transaction::OutputNote;
506 use miden_protocol::{EMPTY_WORD, Felt, Word};
507 use miden_standards::account::auth::{AuthEcdsaK256Keccak, AuthFalcon512Rpo};
508 use miden_standards::note::create_p2id_note;
509 use miden_standards::testing::account_component::MockAccountComponent;
510 use miden_tx::utils::{Deserializable, Serializable};
511
512 use super::{TransactionRequest, TransactionRequestBuilder};
513 use crate::rpc::domain::account::AccountStorageRequirements;
514 use crate::transaction::ForeignAccount;
515
516 #[test]
517 fn transaction_request_serialization() {
518 assert_transaction_request_serialization_with(|| {
519 AuthFalcon512Rpo::new(PublicKeyCommitment::from(EMPTY_WORD)).into()
520 });
521 }
522
523 #[test]
524 fn transaction_request_serialization_ecdsa() {
525 assert_transaction_request_serialization_with(|| {
526 AuthEcdsaK256Keccak::new(PublicKeyCommitment::from(EMPTY_WORD)).into()
527 });
528 }
529
530 fn assert_transaction_request_serialization_with<F>(auth_component: F)
531 where
532 F: FnOnce() -> AccountComponent,
533 {
534 let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
535 let target_id =
536 AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
537 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
538 let mut rng = RpoRandomCoin::new(Word::default());
539
540 let mut notes = vec![];
541 for i in 0..6 {
542 let note = create_p2id_note(
543 sender_id,
544 target_id,
545 vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
546 NoteType::Private,
547 NoteAttachment::default(),
548 &mut rng,
549 )
550 .unwrap();
551 notes.push(note);
552 }
553
554 let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
555 for i in 0..10 {
556 advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
557 }
558
559 let account = AccountBuilder::new(Default::default())
560 .with_component(MockAccountComponent::with_empty_slots())
561 .with_auth_component(auth_component())
562 .account_type(AccountType::RegularAccountImmutableCode)
563 .storage_mode(miden_protocol::account::AccountStorageMode::Private)
564 .build_existing()
565 .unwrap();
566
567 let tx_request = TransactionRequestBuilder::new()
569 .input_notes(vec![(notes.pop().unwrap(), None)])
570 .expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
571 .expected_future_notes(vec![(
572 notes.pop().unwrap().into(),
573 NoteTag::with_account_target(sender_id),
574 )])
575 .extend_advice_map(advice_vec)
576 .foreign_accounts([
577 ForeignAccount::public(
578 target_id,
579 AccountStorageRequirements::new([(
580 StorageSlotName::new("demo::storage_slot").unwrap(),
581 &[Word::default()],
582 )]),
583 )
584 .unwrap(),
585 ForeignAccount::private(&account).unwrap(),
586 ])
587 .own_output_notes(vec![
588 OutputNote::Full(notes.pop().unwrap()),
589 OutputNote::Partial(notes.pop().unwrap().into()),
590 ])
591 .script_arg(rng.draw_word())
592 .auth_arg(rng.draw_word())
593 .build()
594 .unwrap();
595
596 let mut buffer = Vec::new();
597 tx_request.write_into(&mut buffer);
598
599 let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
600 assert_eq!(tx_request, deserialized_tx_request);
601 }
602}