use alloc::{
collections::{BTreeMap, BTreeSet},
string::{String, ToString},
vec::Vec,
};
use core::fmt::{self};
pub use miden_lib::transaction::TransactionKernel;
use miden_objects::{
accounts::{
Account, AccountCode, AccountDelta, AccountHeader, AccountId, AccountStorageHeader,
AccountType,
},
assets::{Asset, NonFungibleAsset},
crypto::merkle::MerklePath,
notes::{Note, NoteDetails, NoteId, NoteTag},
transaction::{InputNotes, TransactionArgs},
vm::AdviceInputs,
AssetError, Digest, Felt, Word, ZERO,
};
pub use miden_tx::{LocalTransactionProver, ProvingOptions, TransactionProver};
use script_builder::{AccountCapabilities, AccountInterface};
use tracing::info;
use super::{Client, FeltRng};
use crate::{
notes::{NoteScreener, NoteUpdates},
store::{
input_note_states::ExpectedNoteState, InputNoteRecord, InputNoteState, NoteFilter,
OutputNoteRecord, TransactionFilter,
},
sync::NoteTagRecord,
ClientError,
};
mod request;
pub use request::{
NoteArgs, PaymentTransactionData, SwapTransactionData, TransactionRequest,
TransactionRequestError, TransactionScriptTemplate,
};
mod script_builder;
pub use miden_objects::transaction::{
ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
TransactionScript,
};
pub use miden_tx::{DataStoreError, TransactionExecutorError};
pub use script_builder::TransactionScriptBuilderError;
#[derive(Clone, Debug)]
pub struct TransactionResult {
transaction: ExecutedTransaction,
relevant_notes: Vec<InputNoteRecord>,
}
impl TransactionResult {
pub async fn new(
transaction: ExecutedTransaction,
note_screener: NoteScreener,
partial_notes: Vec<(NoteDetails, NoteTag)>,
) -> Result<Self, ClientError> {
let mut relevant_notes = vec![];
for note in notes_from_output(transaction.output_notes()) {
let account_relevance = note_screener.check_relevance(note).await?;
if !account_relevance.is_empty() {
relevant_notes.push(note.clone().into());
}
}
relevant_notes.extend(partial_notes.iter().map(|(note_details, tag)| {
InputNoteRecord::new(
note_details.clone(),
None,
ExpectedNoteState {
metadata: None,
after_block_num: 0,
tag: Some(*tag),
}
.into(),
)
}));
let tx_result = Self { transaction, relevant_notes };
Ok(tx_result)
}
pub fn executed_transaction(&self) -> &ExecutedTransaction {
&self.transaction
}
pub fn created_notes(&self) -> &OutputNotes {
self.transaction.output_notes()
}
pub fn relevant_notes(&self) -> &[InputNoteRecord] {
&self.relevant_notes
}
pub fn block_num(&self) -> u32 {
self.transaction.block_header().block_num()
}
pub fn transaction_arguments(&self) -> &TransactionArgs {
self.transaction.tx_args()
}
pub fn account_delta(&self) -> &AccountDelta {
self.transaction.account_delta()
}
pub fn consumed_notes(&self) -> &InputNotes<InputNote> {
self.transaction.tx_inputs().input_notes()
}
}
impl From<TransactionResult> for ExecutedTransaction {
fn from(tx_result: TransactionResult) -> ExecutedTransaction {
tx_result.transaction
}
}
#[derive(Debug, Clone)]
pub struct TransactionRecord {
pub id: TransactionId,
pub account_id: AccountId,
pub init_account_state: Digest,
pub final_account_state: Digest,
pub input_note_nullifiers: Vec<Digest>,
pub output_notes: OutputNotes,
pub transaction_script: Option<TransactionScript>,
pub block_num: u32,
pub transaction_status: TransactionStatus,
}
impl TransactionRecord {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: TransactionId,
account_id: AccountId,
init_account_state: Digest,
final_account_state: Digest,
input_note_nullifiers: Vec<Digest>,
output_notes: OutputNotes,
transaction_script: Option<TransactionScript>,
block_num: u32,
transaction_status: TransactionStatus,
) -> TransactionRecord {
TransactionRecord {
id,
account_id,
init_account_state,
final_account_state,
input_note_nullifiers,
output_notes,
transaction_script,
block_num,
transaction_status,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TransactionStatus {
Pending,
Committed(u32),
Discarded,
}
impl fmt::Display for TransactionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransactionStatus::Pending => write!(f, "Pending"),
TransactionStatus::Committed(block_number) => {
write!(f, "Committed (Block: {})", block_number)
},
TransactionStatus::Discarded => write!(f, "Discarded"),
}
}
}
pub struct TransactionStoreUpdate {
executed_transaction: ExecutedTransaction,
updated_account: Account,
note_updates: NoteUpdates,
new_tags: Vec<NoteTagRecord>,
}
impl TransactionStoreUpdate {
pub fn new(
executed_transaction: ExecutedTransaction,
updated_account: Account,
created_input_notes: Vec<InputNoteRecord>,
created_output_notes: Vec<OutputNoteRecord>,
updated_input_notes: Vec<InputNoteRecord>,
new_tags: Vec<NoteTagRecord>,
) -> Self {
Self {
executed_transaction,
updated_account,
note_updates: NoteUpdates::new(
created_input_notes,
created_output_notes,
updated_input_notes,
vec![],
),
new_tags,
}
}
pub fn executed_transaction(&self) -> &ExecutedTransaction {
&self.executed_transaction
}
pub fn updated_account(&self) -> &Account {
&self.updated_account
}
pub fn note_updates(&self) -> &NoteUpdates {
&self.note_updates
}
pub fn new_tags(&self) -> &[NoteTagRecord] {
&self.new_tags
}
}
impl<R: FeltRng> Client<R> {
pub async fn get_transactions(
&self,
filter: TransactionFilter,
) -> Result<Vec<TransactionRecord>, ClientError> {
self.store.get_transactions(filter).await.map_err(|err| err.into())
}
pub async fn new_transaction(
&mut self,
account_id: AccountId,
transaction_request: TransactionRequest,
) -> Result<TransactionResult, ClientError> {
self.validate_request(account_id, &transaction_request).await?;
let authenticated_input_note_ids: Vec<NoteId> =
transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
let authenticated_note_records = self
.store
.get_input_notes(NoteFilter::List(authenticated_input_note_ids))
.await?;
for authenticated_note_record in authenticated_note_records {
if !authenticated_note_record.is_authenticated() {
return Err(ClientError::TransactionRequestError(
TransactionRequestError::InputNoteNotAuthenticated,
));
}
}
let unauthenticated_input_notes = transaction_request
.unauthenticated_input_notes()
.iter()
.cloned()
.map(|note| note.into())
.collect::<Vec<_>>();
self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
let note_ids = transaction_request.get_input_note_ids();
let output_notes: Vec<Note> =
transaction_request.expected_output_notes().cloned().collect();
let future_notes: Vec<(NoteDetails, NoteTag)> =
transaction_request.expected_future_notes().cloned().collect();
let tx_script = transaction_request
.build_transaction_script(self.get_account_capabilities(account_id).await?)?;
let (foreign_data_advice_inputs, foreign_account_codes, fpi_block_num) =
self.get_foreign_account_inputs(transaction_request.foreign_accounts()).await?;
let tx_args = transaction_request
.into_transaction_args(tx_script)
.with_advice_inputs(foreign_data_advice_inputs);
foreign_account_codes
.iter()
.for_each(|code| self.tx_executor.load_account_code(code));
let block_num = if let Some(block_num) = fpi_block_num {
block_num
} else {
self.store.get_sync_height().await?
};
let executed_transaction = self
.tx_executor
.execute_transaction(account_id, block_num, ¬e_ids, tx_args)
.await?;
let tx_note_auth_hashes: BTreeSet<Digest> =
notes_from_output(executed_transaction.output_notes())
.map(|note| note.hash())
.collect();
let missing_note_ids: Vec<NoteId> = output_notes
.iter()
.filter_map(|n| (!tx_note_auth_hashes.contains(&n.hash())).then_some(n.id()))
.collect();
if !missing_note_ids.is_empty() {
return Err(ClientError::MissingOutputNotes(missing_note_ids));
}
let screener = NoteScreener::new(self.store.clone());
TransactionResult::new(executed_transaction, screener, future_notes).await
}
pub async fn submit_transaction(
&mut self,
tx_result: TransactionResult,
) -> Result<(), ClientError> {
let proven_transaction = self.prove_transaction(&tx_result).await?;
self.submit_proven_transaction(proven_transaction).await?;
self.apply_transaction(tx_result).await
}
async fn prove_transaction(
&mut self,
tx_result: &TransactionResult,
) -> Result<ProvenTransaction, ClientError> {
info!("Proving transaction...");
let proven_transaction =
self.tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
info!("Transaction proven.");
Ok(proven_transaction)
}
async fn submit_proven_transaction(
&mut self,
proven_transaction: ProvenTransaction,
) -> Result<(), ClientError> {
info!("Submitting transaction to the network...");
self.rpc_api.submit_proven_transaction(proven_transaction).await?;
info!("Transaction submitted.");
Ok(())
}
async fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
let transaction_id = tx_result.executed_transaction().id();
let sync_height = self.get_sync_height().await?;
info!("Applying transaction to the local store...");
let account_id = tx_result.executed_transaction().account_id();
let account_delta = tx_result.account_delta();
let (mut account, _seed) = self.get_account(account_id).await?;
account.apply_delta(account_delta)?;
let created_input_notes = tx_result.relevant_notes().to_vec();
let new_tags = created_input_notes
.iter()
.filter_map(|note| {
if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
note.state()
{
Some(NoteTagRecord::with_note_source(*tag, note.id()))
} else {
None
}
})
.collect();
let created_output_notes = tx_result
.created_notes()
.iter()
.cloned()
.filter_map(|output_note| {
OutputNoteRecord::try_from_output_note(output_note, sync_height).ok()
})
.collect::<Vec<_>>();
let consumed_note_ids = tx_result.consumed_notes().iter().map(|note| note.id()).collect();
let consumed_notes = self.get_input_notes(NoteFilter::List(consumed_note_ids)).await?;
let mut updated_input_notes = vec![];
for mut input_note_record in consumed_notes {
if input_note_record.consumed_locally(account_id, transaction_id)? {
updated_input_notes.push(input_note_record);
}
}
let tx_update = TransactionStoreUpdate::new(
tx_result.into(),
account,
created_input_notes,
created_output_notes,
updated_input_notes,
new_tags,
);
self.store.apply_transaction(tx_update).await?;
info!("Transaction stored.");
Ok(())
}
pub fn compile_tx_script<T>(
&self,
inputs: T,
program: &str,
) -> Result<TransactionScript, ClientError>
where
T: IntoIterator<Item = (Word, Vec<Felt>)>,
{
TransactionScript::compile(program, inputs, TransactionKernel::assembler())
.map_err(ClientError::TransactionScriptError)
}
fn get_outgoing_assets(
&self,
transaction_request: &TransactionRequest,
) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
let mut own_notes_assets = match transaction_request.script_template() {
Some(TransactionScriptTemplate::SendNotes(notes)) => {
notes.iter().map(|note| (note.id(), note.assets())).collect::<BTreeMap<_, _>>()
},
_ => Default::default(),
};
let mut output_notes_assets = transaction_request
.expected_output_notes()
.map(|note| (note.id(), note.assets()))
.collect::<BTreeMap<_, _>>();
output_notes_assets.append(&mut own_notes_assets);
let outgoing_assets =
output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
collect_assets(outgoing_assets)
}
async fn get_incoming_assets(
&self,
transaction_request: &TransactionRequest,
) -> Result<(BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>), TransactionRequestError>
{
let incoming_notes_ids: Vec<_> = transaction_request
.input_notes()
.iter()
.filter_map(|(note_id, _)| {
if transaction_request
.unauthenticated_input_notes()
.iter()
.any(|note| note.id() == *note_id)
{
None
} else {
Some(*note_id)
}
})
.collect();
let store_input_notes = self
.get_input_notes(NoteFilter::List(incoming_notes_ids))
.await
.map_err(|err| TransactionRequestError::NoteNotFound(err.to_string()))?;
let all_incoming_assets =
store_input_notes.iter().flat_map(|note| note.assets().iter()).chain(
transaction_request
.unauthenticated_input_notes()
.iter()
.flat_map(|note| note.assets().iter()),
);
Ok(collect_assets(all_incoming_assets))
}
async fn validate_basic_account_request(
&self,
transaction_request: &TransactionRequest,
account: &Account,
) -> Result<(), ClientError> {
let (fungible_balance_map, non_fungible_set) =
self.get_outgoing_assets(transaction_request);
let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
self.get_incoming_assets(transaction_request).await?;
for (faucet_id, amount) in fungible_balance_map {
let account_asset_amount = account.vault().get_balance(faucet_id).unwrap_or(0);
let incoming_balance = incoming_fungible_balance_map.get(&faucet_id).unwrap_or(&0);
if account_asset_amount + incoming_balance < amount {
return Err(ClientError::AssetError(AssetError::AssetAmountNotSufficient(
account_asset_amount,
amount,
)));
}
}
for non_fungible in non_fungible_set {
match account.vault().has_non_fungible_asset(non_fungible.into()) {
Ok(true) => (),
Ok(false) => {
if !incoming_non_fungible_balance_set.contains(&non_fungible) {
return Err(ClientError::AssetError(AssetError::AssetAmountNotSufficient(
0, 1,
)));
}
},
_ => {
return Err(ClientError::AssetError(AssetError::AssetAmountNotSufficient(
0, 1,
)));
},
}
}
Ok(())
}
pub async fn validate_request(
&self,
account_id: AccountId,
transaction_request: &TransactionRequest,
) -> Result<(), ClientError> {
let (account, _) = self.get_account(account_id).await?;
if account.is_faucet() {
Ok(())
} else {
self.validate_basic_account_request(transaction_request, &account).await
}
}
async fn get_account_capabilities(
&self,
account_id: AccountId,
) -> Result<AccountCapabilities, ClientError> {
let account = self.get_account(account_id).await?.0;
let account_auth = self.get_account_auth(account_id).await?;
let account_capabilities = match account.account_type() {
AccountType::FungibleFaucet => AccountInterface::BasicFungibleFaucet,
AccountType::NonFungibleFaucet => todo!("Non fungible faucet not supported yet"),
AccountType::RegularAccountImmutableCode | AccountType::RegularAccountUpdatableCode => {
AccountInterface::BasicWallet
},
};
Ok(AccountCapabilities {
account_id,
auth: account_auth,
interfaces: account_capabilities,
})
}
async fn get_foreign_account_inputs(
&mut self,
account_ids: &BTreeSet<AccountId>,
) -> Result<(AdviceInputs, Vec<AccountCode>, Option<u32>), ClientError> {
let mut advice_inputs = AdviceInputs::default();
let mut account_codes = Vec::new();
if account_ids.is_empty() {
return Ok((AdviceInputs::default(), vec![], None));
}
let (block_num, account_proofs) =
self.rpc_api.get_account_proofs(account_ids, &[], true).await?;
for account_proof in account_proofs.into_iter() {
let account_header = account_proof.account_header().expect("RPC response should include this field becuase `include_headers` is on and no code commitments were sent");
let account_code = account_proof.account_code().expect("RPC response should include this field becuase `include_headers` is on and no code commitments were sent");
let storage_header = account_proof.storage_header().expect("RPC response should include this field becuase `include_headers` is on and no code commitments were sent");
account_codes.push(account_code.clone());
let merkle_path = account_proof.merkle_proof();
extend_advice_inputs_for_account(
&mut advice_inputs,
account_header,
account_code,
storage_header,
merkle_path,
)?;
}
if self.store.get_block_headers(&[block_num]).await?.is_empty() {
info!("Getting current block header data to execute transaction with foreign account requirements");
let summary = self.sync_state().await?;
if summary.block_num != block_num {
let mut current_partial_mmr = self.build_current_partial_mmr(true).await?;
self.get_and_store_authenticated_block(block_num, &mut current_partial_mmr)
.await?;
}
}
Ok((advice_inputs, account_codes, Some(block_num)))
}
}
#[cfg(feature = "testing")]
impl<R: FeltRng> Client<R> {
pub async fn testing_prove_transaction(
&mut self,
tx_result: &TransactionResult,
) -> Result<ProvenTransaction, ClientError> {
self.prove_transaction(tx_result).await
}
pub async fn testing_submit_proven_transaction(
&mut self,
proven_transaction: ProvenTransaction,
) -> Result<(), ClientError> {
self.submit_proven_transaction(proven_transaction).await
}
pub async fn testing_apply_transaction(
&self,
tx_result: TransactionResult,
) -> Result<(), ClientError> {
self.apply_transaction(tx_result).await
}
}
fn extend_advice_inputs_for_account(
advice_inputs: &mut AdviceInputs,
account_header: &AccountHeader,
account_code: &AccountCode,
storage_header: &AccountStorageHeader,
merkle_path: &MerklePath,
) -> Result<(), ClientError> {
let account_id = account_header.id();
let account_nonce = account_header.nonce();
let vault_root = account_header.vault_root();
let storage_root = account_header.storage_commitment();
let code_root = account_header.code_commitment();
let foreign_id_root = Digest::from([account_id.into(), ZERO, ZERO, ZERO]);
let foreign_id_and_nonce = [account_id.into(), ZERO, ZERO, account_nonce];
let mut slots_data = Vec::new();
for (slot_type, value) in storage_header.slots() {
let mut elements = [ZERO; 8];
elements[0..4].copy_from_slice(value);
elements[4..8].copy_from_slice(&slot_type.as_word());
slots_data.extend_from_slice(&elements);
}
advice_inputs.extend_map([
(
foreign_id_root,
[
&foreign_id_and_nonce,
vault_root.as_elements(),
storage_root.as_elements(),
code_root.as_elements(),
]
.concat(),
),
(storage_root, slots_data),
(code_root, account_code.as_elements()),
]);
advice_inputs
.extend_merkle_store(merkle_path.inner_nodes(account_id.into(), account_header.hash())?);
Ok(())
}
fn collect_assets<'a>(
assets: impl Iterator<Item = &'a Asset>,
) -> (BTreeMap<AccountId, u64>, BTreeSet<NonFungibleAsset>) {
let mut fungible_balance_map = BTreeMap::new();
let mut non_fungible_set = BTreeSet::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) => {
non_fungible_set.insert(*non_fungible);
},
});
(fungible_balance_map, non_fungible_set)
}
pub(crate) fn prepare_word(word: &Word) -> String {
word.iter().map(|x| x.as_int().to_string()).collect::<Vec<_>>().join(".")
}
pub fn notes_from_output(output_notes: &OutputNotes) -> impl Iterator<Item = &Note> {
output_notes
.iter()
.filter(|n| matches!(n, OutputNote::Full(_)))
.map(|n| match n {
OutputNote::Full(n) => n,
OutputNote::Header(_) | OutputNote::Partial(_) => {
todo!("For now, all details should be held in OutputNote::Fulls")
},
})
}
#[cfg(test)]
mod test {
use miden_lib::{accounts::auth::RpoFalcon512, transaction::TransactionKernel};
use miden_objects::{
accounts::{
account_id::testing::{
ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN,
ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN,
},
AccountBuilder, AccountComponent, AccountData, StorageMap, StorageSlot,
},
assets::{Asset, FungibleAsset},
crypto::dsa::rpo_falcon512::SecretKey,
notes::NoteType,
testing::account_component::BASIC_WALLET_CODE,
Felt, FieldElement, Word,
};
use super::{PaymentTransactionData, TransactionRequest};
use crate::mock::create_test_client;
#[tokio::test]
async fn test_transaction_creates_two_notes() {
let (mut client, _) = create_test_client().await;
let asset_1: Asset =
FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN.try_into().unwrap(), 123)
.unwrap()
.into();
let asset_2: Asset =
FungibleAsset::new(ACCOUNT_ID_FUNGIBLE_FAUCET_ON_CHAIN.try_into().unwrap(), 500)
.unwrap()
.into();
let secret_key = SecretKey::new();
let wallet_component = AccountComponent::compile(
BASIC_WALLET_CODE,
TransactionKernel::assembler(),
vec![StorageSlot::Value(Word::default()), StorageSlot::Map(StorageMap::default())],
)
.unwrap()
.with_supports_all_types();
let (account, _) = AccountBuilder::new()
.init_seed(Default::default())
.nonce(Felt::ONE)
.with_component(wallet_component)
.with_component(RpoFalcon512::new(secret_key.public_key()))
.with_assets([asset_1, asset_2])
.build()
.unwrap();
client
.import_account(AccountData::new(
account.clone(),
None,
miden_objects::accounts::AuthSecretKey::RpoFalcon512(secret_key.clone()),
))
.await
.unwrap();
client.sync_state().await.unwrap();
let tx_request = TransactionRequest::pay_to_id(
PaymentTransactionData::new(
vec![asset_1, asset_2],
account.id(),
ACCOUNT_ID_REGULAR_ACCOUNT_IMMUTABLE_CODE_ON_CHAIN.try_into().unwrap(),
),
None,
NoteType::Private,
client.rng(),
)
.unwrap();
let tx_result = client.new_transaction(account.id(), tx_request).await.unwrap();
assert!(tx_result
.created_notes()
.get_note(0)
.assets()
.is_some_and(|assets| assets.num_assets() == 2));
client.testing_apply_transaction(tx_result.clone()).await.unwrap();
}
}