use alloc::boxed::Box;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::sync::Arc;
use alloc::vec::Vec;
use miden_protocol::account::{Account, AccountCode, AccountId};
use miden_protocol::asset::NonFungibleAsset;
use miden_protocol::block::BlockNumber;
use miden_protocol::errors::AssetError;
use miden_protocol::note::{Note, NoteDetails, NoteId, NoteRecipient, NoteScript, NoteTag};
use miden_protocol::transaction::AccountInputs;
use miden_protocol::{EMPTY_WORD, Felt, Word};
use miden_standards::account::interface::AccountInterfaceExt;
use miden_tx::{DataStore, NoteConsumptionChecker, TransactionExecutor};
use tracing::info;
use super::Client;
use crate::ClientError;
use crate::note::NoteUpdateTracker;
use crate::rpc::domain::account::AccountStorageRequirements;
use crate::rpc::{AccountStateAt, GrpcError, NodeRpcClient, RpcError};
use crate::store::data_store::ClientDataStore;
use crate::store::input_note_states::ExpectedNoteState;
use crate::store::{
InputNoteRecord,
InputNoteState,
NoteFilter,
OutputNoteRecord,
Store,
TransactionFilter,
};
use crate::sync::NoteTagRecord;
mod prover;
pub use prover::TransactionProver;
mod record;
pub use record::{
DiscardCause,
TransactionDetails,
TransactionRecord,
TransactionStatus,
TransactionStatusVariant,
};
mod store_update;
pub use store_update::TransactionStoreUpdate;
mod request;
pub use request::{
ForeignAccount,
NoteArgs,
PaymentNoteDescription,
SwapTransactionData,
TransactionRequest,
TransactionRequestBuilder,
TransactionRequestError,
TransactionScriptTemplate,
};
mod result;
pub use miden_protocol::transaction::{
ExecutedTransaction,
InputNote,
InputNotes,
OutputNote,
OutputNotes,
ProvenTransaction,
PublicOutputNote,
RawOutputNote,
RawOutputNotes,
TransactionArgs,
TransactionId,
TransactionInputs,
TransactionKernel,
TransactionScript,
TransactionSummary,
};
pub use miden_protocol::vm::{AdviceInputs, AdviceMap};
pub use miden_standards::account::interface::{AccountComponentInterface, AccountInterface};
pub use miden_tx::auth::TransactionAuthenticator;
pub use miden_tx::{
DataStoreError,
LocalTransactionProver,
ProvingOptions,
TransactionExecutorError,
TransactionProverError,
};
pub use result::TransactionResult;
impl<AUTH> Client<AUTH>
where
AUTH: TransactionAuthenticator + Sync + 'static,
{
pub async fn get_transactions(
&self,
filter: TransactionFilter,
) -> Result<Vec<TransactionRecord>, ClientError> {
self.store.get_transactions(filter).await.map_err(Into::into)
}
pub async fn submit_new_transaction(
&mut self,
account_id: AccountId,
transaction_request: TransactionRequest,
) -> Result<TransactionId, ClientError> {
let prover = self.tx_prover.clone();
self.submit_new_transaction_with_prover(account_id, transaction_request, prover)
.await
}
pub async fn submit_new_transaction_with_prover(
&mut self,
account_id: AccountId,
transaction_request: TransactionRequest,
tx_prover: Arc<dyn TransactionProver>,
) -> Result<TransactionId, ClientError> {
if !transaction_request.expected_ntx_scripts().is_empty() {
Box::pin(self.ensure_ntx_scripts_registered(
account_id,
transaction_request.expected_ntx_scripts(),
tx_prover.clone(),
))
.await?;
}
let tx_result = self.execute_transaction(account_id, transaction_request).await?;
let tx_id = tx_result.executed_transaction().id();
let proven_transaction = self.prove_transaction_with(&tx_result, tx_prover).await?;
let submission_height =
self.submit_proven_transaction(proven_transaction, &tx_result).await?;
self.apply_transaction(&tx_result, submission_height).await?;
Ok(tx_id)
}
pub async fn execute_transaction(
&mut self,
account_id: AccountId,
transaction_request: TransactionRequest,
) -> Result<TransactionResult, ClientError> {
self.validate_request(account_id, &transaction_request).await?;
let mut stored_note_records = self
.store
.get_input_notes(NoteFilter::List(transaction_request.input_note_ids().collect()))
.await?;
for note in &stored_note_records {
if note.is_consumed() {
return Err(ClientError::TransactionRequestError(
TransactionRequestError::InputNoteAlreadyConsumed(note.id()),
));
}
}
stored_note_records.retain(InputNoteRecord::is_authenticated);
let authenticated_note_ids =
stored_note_records.iter().map(InputNoteRecord::id).collect::<Vec<_>>();
let unauthenticated_input_notes = transaction_request
.input_notes()
.iter()
.filter(|n| !authenticated_note_ids.contains(&n.id()))
.cloned()
.map(Into::into)
.collect::<Vec<_>>();
self.store.upsert_input_notes(&unauthenticated_input_notes).await?;
let mut notes = transaction_request.build_input_notes(stored_note_records)?;
let output_recipients =
transaction_request.expected_output_recipients().cloned().collect::<Vec<_>>();
let future_notes: Vec<(NoteDetails, NoteTag)> =
transaction_request.expected_future_notes().cloned().collect();
let tx_script = transaction_request.build_transaction_script(
&self.get_account_interface(account_id).await?,
self.source_manager.clone(),
)?;
let foreign_accounts = transaction_request.foreign_accounts().clone();
let (fpi_block_num, foreign_account_inputs) =
self.retrieve_foreign_account_inputs(foreign_accounts).await?;
let ignore_invalid_notes = transaction_request.ignore_invalid_input_notes();
let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
for fpi_account in &foreign_account_inputs {
data_store.mast_store().load_account_code(fpi_account.code());
}
let output_note_scripts: Vec<NoteScript> = transaction_request
.expected_output_recipients()
.map(|n| n.script().clone())
.collect();
self.store.upsert_note_scripts(&output_note_scripts).await?;
let block_num = if let Some(block_num) = fpi_block_num {
block_num
} else {
self.store.get_sync_height().await?
};
let account_record = self
.store
.get_account(account_id)
.await?
.ok_or(ClientError::AccountDataNotFound(account_id))?;
let account: Account = account_record.try_into()?;
data_store.mast_store().load_account_code(account.code());
let tx_args = transaction_request.into_transaction_args(tx_script);
if ignore_invalid_notes {
notes = self.get_valid_input_notes(account, notes, tx_args.clone()).await?;
}
let executed_transaction = self
.build_executor(&data_store)?
.execute_transaction(account_id, block_num, notes, tx_args)
.await?;
validate_executed_transaction(&executed_transaction, &output_recipients)?;
TransactionResult::new(executed_transaction, future_notes)
}
pub async fn prove_transaction(
&mut self,
tx_result: &TransactionResult,
) -> Result<ProvenTransaction, ClientError> {
self.prove_transaction_with(tx_result, self.tx_prover.clone()).await
}
pub async fn prove_transaction_with(
&mut self,
tx_result: &TransactionResult,
tx_prover: Arc<dyn TransactionProver>,
) -> Result<ProvenTransaction, ClientError> {
info!("Proving transaction...");
let proven_transaction =
tx_prover.prove(tx_result.executed_transaction().clone().into()).await?;
info!("Transaction proven.");
Ok(proven_transaction)
}
pub async fn submit_proven_transaction(
&mut self,
proven_transaction: ProvenTransaction,
transaction_inputs: impl Into<TransactionInputs>,
) -> Result<BlockNumber, ClientError> {
info!("Submitting transaction to the network...");
let block_num = self
.rpc_api
.submit_proven_transaction(proven_transaction, transaction_inputs.into())
.await?;
info!("Transaction submitted.");
Ok(block_num)
}
pub async fn get_transaction_store_update(
&self,
tx_result: &TransactionResult,
submission_height: BlockNumber,
) -> Result<TransactionStoreUpdate, ClientError> {
let note_updates = self.get_note_updates(submission_height, tx_result).await?;
let mut new_tags: Vec<NoteTagRecord> = note_updates
.updated_input_notes()
.filter_map(|note| {
let note = note.inner();
if let InputNoteState::Expected(ExpectedNoteState { tag: Some(tag), .. }) =
note.state()
{
Some(NoteTagRecord::with_note_source(*tag, note.id()))
} else {
None
}
})
.collect();
new_tags.extend(note_updates.updated_output_notes().map(|note| {
let note = note.inner();
NoteTagRecord::with_note_source(note.metadata().tag(), note.id())
}));
Ok(TransactionStoreUpdate::new(
tx_result.executed_transaction().clone(),
submission_height,
note_updates,
tx_result.future_notes().to_vec(),
new_tags,
))
}
pub async fn apply_transaction(
&self,
tx_result: &TransactionResult,
submission_height: BlockNumber,
) -> Result<(), ClientError> {
let tx_update = self.get_transaction_store_update(tx_result, submission_height).await?;
self.apply_transaction_update(tx_update).await
}
pub async fn apply_transaction_update(
&self,
tx_update: TransactionStoreUpdate,
) -> Result<(), ClientError> {
info!("Applying transaction to the local store...");
let executed_transaction = tx_update.executed_transaction();
let account_id = executed_transaction.account_id();
if self.account_reader(account_id).status().await?.is_locked() {
return Err(ClientError::AccountLocked(account_id));
}
self.store.apply_transaction(tx_update).await?;
info!("Transaction stored.");
Ok(())
}
pub async fn execute_program(
&mut self,
account_id: AccountId,
tx_script: TransactionScript,
advice_inputs: AdviceInputs,
foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
) -> Result<[Felt; 16], ClientError> {
let (fpi_block_number, foreign_account_inputs) =
self.retrieve_foreign_account_inputs(foreign_accounts).await?;
let block_ref = if let Some(block_number) = fpi_block_number {
block_number
} else {
self.get_sync_height().await?
};
let account_record = self
.store
.get_account(account_id)
.await?
.ok_or(ClientError::AccountDataNotFound(account_id))?;
let account: Account = account_record.try_into()?;
let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
data_store.register_foreign_account_inputs(foreign_account_inputs.iter().cloned());
data_store.mast_store().load_account_code(account.code());
for fpi_account in &foreign_account_inputs {
data_store.mast_store().load_account_code(fpi_account.code());
}
Ok(self
.build_executor(&data_store)?
.execute_tx_view_script(account_id, block_ref, tx_script, advice_inputs)
.await?)
}
async fn get_note_updates(
&self,
submission_height: BlockNumber,
tx_result: &TransactionResult,
) -> Result<NoteUpdateTracker, ClientError> {
let executed_tx = tx_result.executed_transaction();
let current_timestamp = self.store.get_current_timestamp();
let current_block_num = self.store.get_sync_height().await?;
let new_output_notes = executed_tx
.output_notes()
.iter()
.cloned()
.filter_map(|output_note| {
OutputNoteRecord::try_from_output_note(output_note, submission_height).ok()
})
.collect::<Vec<_>>();
let mut new_input_notes = vec![];
let output_notes =
notes_from_output(executed_tx.output_notes()).cloned().collect::<Vec<_>>();
let note_screener = self.note_screener();
let output_note_relevances = note_screener.can_consume_batch(&output_notes).await?;
for note in output_notes {
if output_note_relevances.contains_key(¬e.id()) {
let metadata = note.metadata().clone();
let tag = metadata.tag();
new_input_notes.push(InputNoteRecord::new(
note.into(),
current_timestamp,
ExpectedNoteState {
metadata: Some(metadata),
after_block_num: submission_height,
tag: Some(tag),
}
.into(),
));
}
}
new_input_notes.extend(tx_result.future_notes().iter().map(|(note_details, tag)| {
InputNoteRecord::new(
note_details.clone(),
None,
ExpectedNoteState {
metadata: None,
after_block_num: current_block_num,
tag: Some(*tag),
}
.into(),
)
}));
let consumed_note_ids =
executed_tx.tx_inputs().input_notes().iter().map(InputNote::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(
executed_tx.account_id(),
executed_tx.id(),
self.store.get_current_timestamp(),
)? {
updated_input_notes.push(input_note_record);
}
}
Ok(NoteUpdateTracker::for_transaction_updates(
new_input_notes,
updated_input_notes,
new_output_notes,
))
}
pub async fn validate_request(
&mut self,
account_id: AccountId,
transaction_request: &TransactionRequest,
) -> Result<(), ClientError> {
if let Some(max_block_number_delta) = self.max_block_number_delta {
let current_chain_tip =
self.rpc_api.get_block_header_by_number(None, false).await?.0.block_num();
if current_chain_tip > self.store.get_sync_height().await? + max_block_number_delta {
return Err(ClientError::RecencyConditionError(
"The client is too far behind the chain tip to execute the transaction",
));
}
}
let account = self.try_get_account(account_id).await?;
if account.is_faucet() {
Ok(())
} else {
validate_basic_account_request(transaction_request, &account)
}
}
pub async fn ensure_ntx_scripts_registered(
&mut self,
account_id: AccountId,
scripts: &[NoteScript],
tx_prover: Arc<dyn TransactionProver>,
) -> Result<(), ClientError> {
let mut missing_scripts = Vec::new();
for script in scripts {
let script_root = script.root();
match self.rpc_api.get_note_script_by_root(script_root).await {
Ok(_) => {},
Err(RpcError::RequestError { error_kind: GrpcError::NotFound, .. }) => {
missing_scripts.push(script.clone());
},
Err(other) => {
return Err(ClientError::NtxScriptRegistrationFailed {
script_root,
source: other,
});
},
}
}
if missing_scripts.is_empty() {
return Ok(());
}
let registration_request = TransactionRequestBuilder::new().build_register_note_scripts(
account_id,
missing_scripts,
self.rng(),
)?;
let tx_result = self.execute_transaction(account_id, registration_request).await?;
let proven = self.prove_transaction_with(&tx_result, tx_prover).await?;
let submission_height = self.submit_proven_transaction(proven, &tx_result).await?;
self.apply_transaction(&tx_result, submission_height).await?;
Ok(())
}
async fn get_valid_input_notes(
&self,
account: Account,
mut input_notes: InputNotes<InputNote>,
tx_args: TransactionArgs,
) -> Result<InputNotes<InputNote>, ClientError> {
loop {
let data_store = ClientDataStore::new(self.store.clone(), self.rpc_api.clone());
data_store.mast_store().load_account_code(account.code());
let execution = NoteConsumptionChecker::new(&self.build_executor(&data_store)?)
.check_notes_consumability(
account.id(),
self.store.get_sync_height().await?,
input_notes.iter().map(|n| n.clone().into_note()).collect(),
tx_args.clone(),
)
.await?;
if execution.failed.is_empty() {
break;
}
let failed_note_ids: BTreeSet<NoteId> =
execution.failed.iter().map(|n| n.note.id()).collect();
let filtered_input_notes = InputNotes::new(
input_notes
.into_iter()
.filter(|note| !failed_note_ids.contains(¬e.id()))
.collect(),
)
.expect("Created from a valid input notes list");
input_notes = filtered_input_notes;
}
Ok(input_notes)
}
pub(crate) async fn get_account_interface(
&self,
account_id: AccountId,
) -> Result<AccountInterface, ClientError> {
let account = self.try_get_account(account_id).await?;
Ok(AccountInterface::from_account(&account))
}
async fn retrieve_foreign_account_inputs(
&mut self,
foreign_accounts: BTreeMap<AccountId, ForeignAccount>,
) -> Result<(Option<BlockNumber>, Vec<AccountInputs>), ClientError> {
if foreign_accounts.is_empty() {
return Ok((None, Vec::new()));
}
let block_num = self.get_sync_height().await?;
let mut return_foreign_account_inputs = Vec::with_capacity(foreign_accounts.len());
for foreign_account in foreign_accounts.into_values() {
let foreign_account_inputs = match foreign_account {
ForeignAccount::Public(account_id, storage_requirements) => {
fetch_public_account_inputs(
&self.store,
&self.rpc_api,
account_id,
storage_requirements,
AccountStateAt::Block(block_num),
)
.await?
},
ForeignAccount::Private(partial_account) => {
let account_id = partial_account.id();
let (_, account_proof) = self
.rpc_api
.get_account_proof(
account_id,
AccountStorageRequirements::default(),
AccountStateAt::Block(block_num),
None,
None,
)
.await?;
let (witness, _) = account_proof.into_parts();
AccountInputs::new(partial_account, witness)
},
};
return_foreign_account_inputs.push(foreign_account_inputs);
}
Ok((Some(block_num), return_foreign_account_inputs))
}
pub(crate) fn build_executor<'store, 'auth, STORE: DataStore + Sync>(
&'auth self,
data_store: &'store STORE,
) -> Result<TransactionExecutor<'store, 'auth, STORE, AUTH>, TransactionExecutorError> {
let mut executor = TransactionExecutor::new(data_store).with_options(self.exec_options)?;
if let Some(authenticator) = self.authenticator.as_deref() {
executor = executor.with_authenticator(authenticator);
}
executor = executor.with_source_manager(self.source_manager.clone());
Ok(executor)
}
}
fn get_outgoing_assets(
transaction_request: &TransactionRequest,
) -> (BTreeMap<AccountId, u64>, Vec<NonFungibleAsset>) {
let mut own_notes_assets = match transaction_request.script_template() {
Some(TransactionScriptTemplate::SendNotes(notes)) => notes
.iter()
.map(|note| (note.id(), note.assets().clone()))
.collect::<BTreeMap<_, _>>(),
_ => BTreeMap::default(),
};
let mut output_notes_assets = transaction_request
.expected_output_own_notes()
.into_iter()
.map(|note| (note.id(), note.assets().clone()))
.collect::<BTreeMap<_, _>>();
output_notes_assets.append(&mut own_notes_assets);
let outgoing_assets = output_notes_assets.values().flat_map(|note_assets| note_assets.iter());
request::collect_assets(outgoing_assets)
}
fn validate_basic_account_request(
transaction_request: &TransactionRequest,
account: &Account,
) -> Result<(), ClientError> {
let (fungible_balance_map, non_fungible_set) = get_outgoing_assets(transaction_request);
let (incoming_fungible_balance_map, incoming_non_fungible_balance_set) =
transaction_request.incoming_assets();
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::FungibleAssetAmountNotSufficient {
minuend: account_asset_amount,
subtrahend: amount,
}));
}
}
for non_fungible in &non_fungible_set {
match account.vault().has_non_fungible_asset(*non_fungible) {
Ok(true) => (),
Ok(false) => {
if !incoming_non_fungible_balance_set.contains(non_fungible) {
return Err(ClientError::AssetError(
AssetError::NonFungibleFaucetIdTypeMismatch(non_fungible.faucet_id()),
));
}
},
_ => {
return Err(ClientError::AssetError(AssetError::NonFungibleFaucetIdTypeMismatch(
non_fungible.faucet_id(),
)));
},
}
}
Ok(())
}
pub(crate) async fn fetch_public_account_inputs(
store: &Arc<dyn Store>,
rpc_api: &Arc<dyn NodeRpcClient>,
account_id: AccountId,
storage_requirements: AccountStorageRequirements,
account_state_at: AccountStateAt,
) -> Result<AccountInputs, ClientError> {
let known_account_code: Option<AccountCode> =
store.get_foreign_account_code(vec![account_id]).await?.into_values().next();
let (_, account_proof) = rpc_api
.get_account_proof(
account_id,
storage_requirements.clone(),
account_state_at,
known_account_code,
Some(EMPTY_WORD),
)
.await?;
let account_inputs = request::account_proof_into_inputs(account_proof, &storage_requirements)?;
let _ = store
.upsert_foreign_account_code(account_id, account_inputs.code().clone())
.await
.inspect_err(|err| {
tracing::warn!(
%account_id,
%err,
"Failed to persist foreign account code to store"
);
});
Ok(account_inputs)
}
pub fn notes_from_output(output_notes: &RawOutputNotes) -> impl Iterator<Item = &Note> {
output_notes.iter().filter_map(|n| match n {
RawOutputNote::Full(n) => Some(n),
RawOutputNote::Partial(_) => None,
})
}
fn validate_executed_transaction(
executed_transaction: &ExecutedTransaction,
expected_output_recipients: &[NoteRecipient],
) -> Result<(), ClientError> {
let tx_output_recipient_digests = executed_transaction
.output_notes()
.iter()
.filter_map(|n| n.recipient().map(NoteRecipient::digest))
.collect::<Vec<_>>();
let missing_recipient_digest: Vec<Word> = expected_output_recipients
.iter()
.filter_map(|recipient| {
(!tx_output_recipient_digests.contains(&recipient.digest()))
.then_some(recipient.digest())
})
.collect();
if !missing_recipient_digest.is_empty() {
return Err(ClientError::MissingOutputRecipients(missing_recipient_digest));
}
Ok(())
}