use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::assembly::SourceManagerSync;
use miden_protocol::asset::{Asset, NonFungibleAsset};
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::crypto::merkle::store::MerkleStore;
use miden_protocol::errors::{
AccountError,
AssetVaultError,
NoteError,
StorageMapError,
TransactionInputError,
TransactionScriptError,
};
use miden_protocol::note::{
Note,
NoteDetails,
NoteId,
NoteRecipient,
NoteScript,
NoteTag,
PartialNote,
};
use miden_protocol::transaction::{InputNote, InputNotes, TransactionArgs, TransactionScript};
use miden_protocol::vm::AdviceMap;
use miden_standards::account::interface::{AccountInterface, AccountInterfaceError};
use miden_standards::code_builder::CodeBuilder;
use miden_standards::errors::CodeBuilderError;
use miden_tx::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
use thiserror::Error;
mod builder;
pub use builder::{PaymentNoteDescription, SwapTransactionData, TransactionRequestBuilder};
mod foreign;
pub use foreign::ForeignAccount;
pub(crate) use foreign::account_proof_into_inputs;
use crate::store::InputNoteRecord;
pub type NoteArgs = Word;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum TransactionScriptTemplate {
CustomScript(TransactionScript),
SendNotes(Vec<PartialNote>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TransactionRequest {
input_notes: Vec<Note>,
input_notes_args: Vec<(NoteId, Option<NoteArgs>)>,
script_template: Option<TransactionScriptTemplate>,
expected_output_recipients: BTreeMap<Word, NoteRecipient>,
expected_future_notes: BTreeMap<NoteId, (NoteDetails, NoteTag)>,
advice_map: AdviceMap,
merkle_store: MerkleStore,
foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
expiration_delta: Option<u16>,
ignore_invalid_input_notes: bool,
script_arg: Option<Word>,
auth_arg: Option<Word>,
expected_ntx_scripts: Vec<NoteScript>,
}
impl TransactionRequest {
pub fn input_notes(&self) -> &[Note] {
&self.input_notes
}
pub fn input_note_ids(&self) -> impl Iterator<Item = NoteId> {
self.input_notes.iter().map(Note::id)
}
pub fn incoming_assets(&self) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
collect_assets(self.input_notes.iter().flat_map(|note| note.assets().iter()))
}
pub fn get_note_args(&self) -> BTreeMap<NoteId, NoteArgs> {
self.input_notes_args
.iter()
.filter_map(|(note, args)| args.map(|a| (*note, a)))
.collect()
}
pub fn expected_output_own_notes(&self) -> Vec<Note> {
match &self.script_template {
Some(TransactionScriptTemplate::SendNotes(notes)) => notes
.iter()
.map(|partial| {
Note::new(
partial.assets().clone(),
partial.metadata().clone(),
self.expected_output_recipients
.get(&partial.recipient_digest())
.expect("Recipient should be included if it's an own note")
.clone(),
)
})
.collect(),
_ => vec![],
}
}
pub fn expected_output_recipients(&self) -> impl Iterator<Item = &NoteRecipient> {
self.expected_output_recipients.values()
}
pub fn expected_future_notes(&self) -> impl Iterator<Item = &(NoteDetails, NoteTag)> {
self.expected_future_notes.values()
}
pub fn script_template(&self) -> &Option<TransactionScriptTemplate> {
&self.script_template
}
pub fn advice_map(&self) -> &AdviceMap {
&self.advice_map
}
pub fn advice_map_mut(&mut self) -> &mut AdviceMap {
&mut self.advice_map
}
pub fn merkle_store(&self) -> &MerkleStore {
&self.merkle_store
}
pub fn foreign_accounts(&self) -> &BTreeMap<AccountId, ForeignAccount> {
&self.foreign_accounts
}
pub fn ignore_invalid_input_notes(&self) -> bool {
self.ignore_invalid_input_notes
}
pub fn script_arg(&self) -> &Option<Word> {
&self.script_arg
}
pub fn auth_arg(&self) -> &Option<Word> {
&self.auth_arg
}
pub fn expected_ntx_scripts(&self) -> &[NoteScript] {
&self.expected_ntx_scripts
}
pub(crate) fn build_input_notes(
&self,
authenticated_note_records: Vec<InputNoteRecord>,
) -> Result<InputNotes<InputNote>, TransactionRequestError> {
let mut input_notes: BTreeMap<NoteId, InputNote> = BTreeMap::new();
for authenticated_note_record in authenticated_note_records {
if !authenticated_note_record.is_authenticated() {
return Err(TransactionRequestError::InputNoteNotAuthenticated(
authenticated_note_record.id(),
));
}
if authenticated_note_record.is_consumed() {
return Err(TransactionRequestError::InputNoteAlreadyConsumed(
authenticated_note_record.id(),
));
}
let authenticated_note_id = authenticated_note_record.id();
input_notes.insert(
authenticated_note_id,
authenticated_note_record
.try_into()
.expect("Authenticated note record should be convertible to InputNote"),
);
}
let authenticated_note_ids: BTreeSet<NoteId> = input_notes.keys().copied().collect();
for note in self.input_notes().iter().filter(|n| !authenticated_note_ids.contains(&n.id()))
{
input_notes.insert(note.id(), InputNote::Unauthenticated { note: note.clone() });
}
Ok(InputNotes::new(
self.input_note_ids()
.map(|note_id| {
input_notes
.remove(¬e_id)
.expect("The input note map was checked to contain all input notes")
})
.collect(),
)?)
}
pub(crate) fn into_transaction_args(self, tx_script: TransactionScript) -> TransactionArgs {
let note_args = self.get_note_args();
let TransactionRequest {
expected_output_recipients,
advice_map,
merkle_store,
..
} = self;
let mut tx_args = TransactionArgs::new(advice_map).with_note_args(note_args);
tx_args = if let Some(argument) = self.script_arg {
tx_args.with_tx_script_and_args(tx_script, argument)
} else {
tx_args.with_tx_script(tx_script)
};
if let Some(auth_argument) = self.auth_arg {
tx_args = tx_args.with_auth_args(auth_argument);
}
tx_args
.extend_output_note_recipients(expected_output_recipients.into_values().map(Box::new));
tx_args.extend_merkle_store(merkle_store.inner_nodes());
tx_args
}
pub(crate) fn build_transaction_script(
&self,
account_interface: &AccountInterface,
source_manager: Arc<dyn SourceManagerSync>,
) -> Result<TransactionScript, TransactionRequestError> {
match &self.script_template {
Some(TransactionScriptTemplate::CustomScript(script)) => Ok(script.clone()),
Some(TransactionScriptTemplate::SendNotes(notes)) => {
Ok(account_interface.build_send_notes_script(notes, self.expiration_delta)?)
},
None => {
let empty_script = CodeBuilder::with_source_manager(source_manager)
.compile_tx_script("begin nop end")?;
Ok(empty_script)
},
}
}
}
impl Serializable for TransactionRequest {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
self.input_notes.write_into(target);
self.input_notes_args.write_into(target);
match &self.script_template {
None => target.write_u8(0),
Some(TransactionScriptTemplate::CustomScript(script)) => {
target.write_u8(1);
script.write_into(target);
},
Some(TransactionScriptTemplate::SendNotes(notes)) => {
target.write_u8(2);
notes.write_into(target);
},
}
self.expected_output_recipients.write_into(target);
self.expected_future_notes.write_into(target);
self.advice_map.write_into(target);
self.merkle_store.write_into(target);
let foreign_accounts: Vec<_> = self.foreign_accounts.values().cloned().collect();
foreign_accounts.write_into(target);
self.expiration_delta.write_into(target);
target.write_u8(u8::from(self.ignore_invalid_input_notes));
self.script_arg.write_into(target);
self.auth_arg.write_into(target);
self.expected_ntx_scripts.write_into(target);
}
}
impl Deserializable for TransactionRequest {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let input_notes = Vec::<Note>::read_from(source)?;
let input_notes_args = Vec::<(NoteId, Option<NoteArgs>)>::read_from(source)?;
let script_template = match source.read_u8()? {
0 => None,
1 => {
let transaction_script = TransactionScript::read_from(source)?;
Some(TransactionScriptTemplate::CustomScript(transaction_script))
},
2 => {
let notes = Vec::<PartialNote>::read_from(source)?;
Some(TransactionScriptTemplate::SendNotes(notes))
},
_ => {
return Err(DeserializationError::InvalidValue(
"Invalid script template type".to_string(),
));
},
};
let expected_output_recipients = BTreeMap::<Word, NoteRecipient>::read_from(source)?;
let expected_future_notes = BTreeMap::<NoteId, (NoteDetails, NoteTag)>::read_from(source)?;
let advice_map = AdviceMap::read_from(source)?;
let merkle_store = MerkleStore::read_from(source)?;
let mut foreign_accounts = BTreeMap::new();
for foreign_account in Vec::<ForeignAccount>::read_from(source)? {
foreign_accounts.entry(foreign_account.account_id()).or_insert(foreign_account);
}
let expiration_delta = Option::<u16>::read_from(source)?;
let ignore_invalid_input_notes = source.read_u8()? == 1;
let script_arg = Option::<Word>::read_from(source)?;
let auth_arg = Option::<Word>::read_from(source)?;
let expected_ntx_scripts = Vec::<NoteScript>::read_from(source)?;
Ok(TransactionRequest {
input_notes,
input_notes_args,
script_template,
expected_output_recipients,
expected_future_notes,
advice_map,
merkle_store,
foreign_accounts,
expiration_delta,
ignore_invalid_input_notes,
script_arg,
auth_arg,
expected_ntx_scripts,
})
}
}
pub(crate) fn collect_assets<'a>(
assets: impl Iterator<Item = &'a Asset>,
) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
let mut fungible_balance_map = BTreeMap::new();
let mut non_fungible_set = Vec::new();
assets.for_each(|asset| match asset {
Asset::Fungible(fungible) => {
fungible_balance_map
.entry(fungible.faucet_id())
.and_modify(|balance| *balance += fungible.amount())
.or_insert(fungible.amount());
},
Asset::NonFungible(non_fungible) => {
if !non_fungible_set.contains(non_fungible) {
non_fungible_set.push(*non_fungible);
}
},
});
(fungible_balance_map, non_fungible_set)
}
impl Default for TransactionRequestBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Error)]
pub enum TransactionRequestError {
#[error("account interface error")]
AccountInterfaceError(#[from] AccountInterfaceError),
#[error("account error")]
AccountError(#[from] AccountError),
#[error("duplicate input note: note {0} was added more than once to the transaction")]
DuplicateInputNote(NoteId),
#[error(
"the account proof does not contain the required foreign account data; re-fetch the proof and retry"
)]
ForeignAccountDataMissing,
#[error(
"foreign account {0} has an incompatible storage mode; use `ForeignAccount::public()` for public accounts and `ForeignAccount::private()` for private accounts"
)]
InvalidForeignAccountId(AccountId),
#[error(
"note {0} cannot be used as an authenticated input: it does not have a valid inclusion proof"
)]
InputNoteNotAuthenticated(NoteId),
#[error("note {0} has already been consumed")]
InputNoteAlreadyConsumed(NoteId),
#[error("sender account {0} is not tracked by this client or does not exist")]
InvalidSenderAccount(AccountId),
#[error("invalid transaction script")]
InvalidTransactionScript(#[from] TransactionScriptError),
#[error("merkle proof error")]
MerkleError(#[from] MerkleError),
#[error("empty transaction: the request has no input notes and no account state changes")]
NoInputNotesNorAccountChange,
#[error("note not found: {0}")]
NoteNotFound(String),
#[error("failed to create note")]
NoteCreationError(#[from] NoteError),
#[error("pay-to-ID note must contain at least one asset to transfer")]
P2IDNoteWithoutAsset,
#[error("error building script")]
CodeBuilderError(#[from] CodeBuilderError),
#[error("transaction script template error: {0}")]
ScriptTemplateError(String),
#[error("storage slot {0} not found in account ID {1}")]
StorageSlotNotFound(u8, AccountId),
#[error("error while building the input notes")]
TransactionInputError(#[from] TransactionInputError),
#[error("account storage map error")]
StorageMapError(#[from] StorageMapError),
#[error("asset vault error")]
AssetVaultError(#[from] AssetVaultError),
#[error(
"unsupported authentication scheme ID {0}; supported schemes are: RpoFalcon512 (0) and EcdsaK256Keccak (1)"
)]
UnsupportedAuthSchemeId(u8),
}
#[cfg(test)]
mod tests {
use std::vec::Vec;
use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
use miden_protocol::account::{
AccountBuilder,
AccountComponent,
AccountId,
AccountType,
StorageMapKey,
StorageSlotName,
};
use miden_protocol::asset::FungibleAsset;
use miden_protocol::crypto::rand::{FeltRng, RandomCoin};
use miden_protocol::note::{NoteAttachment, NoteTag, NoteType};
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
ACCOUNT_ID_SENDER,
};
use miden_protocol::{EMPTY_WORD, Felt, Word};
use miden_standards::account::auth::AuthSingleSig;
use miden_standards::note::P2idNote;
use miden_standards::testing::account_component::MockAccountComponent;
use miden_tx::utils::serde::{Deserializable, Serializable};
use super::{TransactionRequest, TransactionRequestBuilder};
use crate::rpc::domain::account::AccountStorageRequirements;
use crate::transaction::ForeignAccount;
#[test]
fn transaction_request_serialization() {
assert_transaction_request_serialization_with(|| {
AuthSingleSig::new(
PublicKeyCommitment::from(EMPTY_WORD),
AuthScheme::Falcon512Poseidon2,
)
.into()
});
}
#[test]
fn transaction_request_serialization_ecdsa() {
assert_transaction_request_serialization_with(|| {
AuthSingleSig::new(PublicKeyCommitment::from(EMPTY_WORD), AuthScheme::EcdsaK256Keccak)
.into()
});
}
fn assert_transaction_request_serialization_with<F>(auth_component: F)
where
F: FnOnce() -> AccountComponent,
{
let sender_id = AccountId::try_from(ACCOUNT_ID_SENDER).unwrap();
let target_id =
AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
let mut rng = RandomCoin::new(Word::default());
let mut notes = vec![];
for i in 0..6 {
let note = P2idNote::create(
sender_id,
target_id,
vec![FungibleAsset::new(faucet_id, 100 + i).unwrap().into()],
NoteType::Private,
NoteAttachment::default(),
&mut rng,
)
.unwrap();
notes.push(note);
}
let mut advice_vec: Vec<(Word, Vec<Felt>)> = vec![];
for i in 0..10 {
advice_vec.push((rng.draw_word(), vec![Felt::new(i)]));
}
let account = AccountBuilder::new(Default::default())
.with_component(MockAccountComponent::with_empty_slots())
.with_auth_component(auth_component())
.account_type(AccountType::RegularAccountImmutableCode)
.storage_mode(miden_protocol::account::AccountStorageMode::Private)
.build_existing()
.unwrap();
let tx_request = TransactionRequestBuilder::new()
.input_notes(vec![(notes.pop().unwrap(), None)])
.expected_output_recipients(vec![notes.pop().unwrap().recipient().clone()])
.expected_future_notes(vec![(
notes.pop().unwrap().into(),
NoteTag::with_account_target(sender_id),
)])
.extend_advice_map(advice_vec)
.foreign_accounts([
ForeignAccount::public(
target_id,
AccountStorageRequirements::new([(
StorageSlotName::new("demo::storage_slot").unwrap(),
&[StorageMapKey::new(Word::default())],
)]),
)
.unwrap(),
ForeignAccount::private(&account).unwrap(),
])
.own_output_notes(vec![notes.pop().unwrap(), notes.pop().unwrap()])
.script_arg(rng.draw_word())
.auth_arg(rng.draw_word())
.expected_ntx_scripts(vec![notes.first().unwrap().recipient().script().clone()])
.build()
.unwrap();
let mut buffer = Vec::new();
tx_request.write_into(&mut buffer);
let deserialized_tx_request = TransactionRequest::read_from_bytes(&buffer).unwrap();
assert_eq!(tx_request, deserialized_tx_request);
}
}