use alloc::{
collections::{BTreeMap, BTreeSet},
string::{String, ToString},
vec::Vec,
};
use core::fmt;
use miden_lib::transaction::TransactionKernel;
use miden_objects::{
accounts::{Account, AccountDelta, AccountId, AccountType},
assets::{Asset, NonFungibleAsset},
notes::{Note, NoteDetails, NoteExecutionMode, NoteId, NoteTag, NoteType},
transaction::{InputNotes, TransactionArgs},
AssetError, Digest, Felt, NoteError, Word,
};
use miden_tx::{auth::TransactionAuthenticator, ProvingOptions, TransactionProver};
use request::{TransactionRequestError, TransactionScriptTemplate};
use script_builder::{AccountCapabilities, AccountInterface, TransactionScriptBuilder};
use tracing::info;
use winter_maybe_async::{maybe_async, maybe_await};
use self::request::TransactionRequest;
use super::{rpc::NodeRpcClient, Client, FeltRng};
use crate::{
notes::NoteScreener,
store::{InputNoteRecord, NoteFilter, Store, TransactionFilter},
ClientError,
};
pub mod request;
pub mod script_builder;
pub use miden_objects::transaction::{
ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
TransactionScript,
};
pub use miden_tx::{DataStoreError, TransactionExecutorError};
pub use request::known_script_roots;
#[derive(Clone, Debug)]
pub struct TransactionResult {
transaction: ExecutedTransaction,
relevant_notes: Vec<InputNoteRecord>,
}
impl TransactionResult {
#[maybe_async]
pub fn new<S: Store>(
transaction: ExecutedTransaction,
note_screener: NoteScreener<S>,
partial_notes: Vec<NoteDetails>,
) -> Result<Self, ClientError> {
let mut relevant_notes = vec![];
for note in notes_from_output(transaction.output_notes()) {
let account_relevance = maybe_await!(note_screener.check_relevance(note))?;
if !account_relevance.is_empty() {
relevant_notes.push(note.clone().into());
}
}
relevant_notes.extend(partial_notes.iter().map(InputNoteRecord::from));
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()
}
}
#[derive(Debug)]
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),
}
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)
},
}
}
}
impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> {
#[maybe_async]
pub fn get_transactions(
&self,
filter: TransactionFilter,
) -> Result<Vec<TransactionRecord>, ClientError> {
maybe_await!(self.store.get_transactions(filter)).map_err(|err| err.into())
}
#[maybe_async]
pub fn new_transaction(
&mut self,
account_id: AccountId,
transaction_request: TransactionRequest,
) -> Result<TransactionResult, ClientError> {
maybe_await!(self.validate_request(account_id, &transaction_request))?;
let authenticated_input_note_ids: Vec<NoteId> =
transaction_request.authenticated_input_note_ids().collect::<Vec<_>>();
let authenticated_note_records = maybe_await!(self
.store
.get_input_notes(NoteFilter::List(&authenticated_input_note_ids)))?;
for authenticated_note_record in authenticated_note_records {
if !authenticated_note_record.is_authenticated() {
return Err(ClientError::TransactionRequestError(
TransactionRequestError::InputNoteNotAuthenticated,
));
}
}
for unauthenticated_input_note in transaction_request.unauthenticated_input_notes() {
maybe_await!(self.store.insert_input_note(unauthenticated_input_note.clone().into()))?;
}
let block_num = maybe_await!(self.store.get_sync_height())?;
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> =
transaction_request.expected_future_notes().cloned().collect();
let tx_script = match transaction_request.script_template() {
Some(TransactionScriptTemplate::CustomScript(script)) => script.clone(),
Some(TransactionScriptTemplate::SendNotes(notes)) => {
let tx_script_builder = TransactionScriptBuilder::new(maybe_await!(
self.get_account_capabilities(account_id)
)?);
tx_script_builder.build_send_notes_script(notes)?
},
None => {
if transaction_request.input_notes().is_empty() {
return Err(ClientError::TransactionRequestError(
TransactionRequestError::NoInputNotes,
));
}
let tx_script_builder = TransactionScriptBuilder::new(maybe_await!(
self.get_account_capabilities(account_id)
)?);
tx_script_builder.build_auth_script()?
},
};
let tx_args = transaction_request.into_transaction_args(tx_script);
let executed_transaction = maybe_await!(self
.tx_executor
.execute_transaction(account_id, block_num, ¬e_ids, tx_args,))?;
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());
maybe_await!(TransactionResult::new(executed_transaction, screener, future_notes))
}
pub async fn submit_transaction(
&mut self,
tx_result: TransactionResult,
) -> Result<(), ClientError> {
let proven_transaction = self.prove_transaction(&tx_result)?;
self.submit_proven_transaction(proven_transaction).await?;
maybe_await!(self.apply_transaction(tx_result))
}
fn prove_transaction(
&mut self,
tx_result: &TransactionResult,
) -> Result<ProvenTransaction, ClientError> {
let transaction_prover = TransactionProver::new(ProvingOptions::default());
info!("Proving transaction...");
let proven_transaction =
transaction_prover.prove_transaction(tx_result.executed_transaction().clone())?;
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(())
}
#[maybe_async]
fn apply_transaction(&self, tx_result: TransactionResult) -> Result<(), ClientError> {
info!("Applying transaction to the local store...");
maybe_await!(self.store.apply_transaction(tx_result))?;
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)
}
#[maybe_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 =
maybe_await!(self.get_input_notes(NoteFilter::List(&incoming_notes_ids)))
.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))
}
#[maybe_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) =
maybe_await!(self.get_incoming_assets(transaction_request))?;
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(())
}
#[maybe_async]
pub fn validate_request(
&self,
account_id: AccountId,
transaction_request: &TransactionRequest,
) -> Result<(), ClientError> {
let (account, _) = maybe_await!(self.get_account(account_id))?;
if account.is_faucet() {
Ok(())
} else {
maybe_await!(self.validate_basic_account_request(transaction_request, &account))
}
}
#[maybe_async]
fn get_account_capabilities(
&self,
account_id: AccountId,
) -> Result<AccountCapabilities, ClientError> {
let account = maybe_await!(self.get_account(account_id))?.0;
let account_auth = maybe_await!(self.get_account_auth(account_id))?;
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,
})
}
}
#[cfg(feature = "testing")]
impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client<N, R, S, A> {
pub fn testing_prove_transaction(
&mut self,
tx_result: &TransactionResult,
) -> Result<ProvenTransaction, ClientError> {
self.prove_transaction(tx_result)
}
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> {
maybe_await!(self.apply_transaction(tx_result))
}
}
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")
},
})
}
pub fn build_swap_tag(
note_type: NoteType,
offered_asset_faucet_id: AccountId,
requested_asset_faucet_id: AccountId,
) -> Result<NoteTag, NoteError> {
const SWAP_USE_CASE_ID: u16 = 0;
let offered_asset_id: u64 = offered_asset_faucet_id.into();
let offered_asset_tag = (offered_asset_id >> 52) as u8;
let requested_asset_id: u64 = requested_asset_faucet_id.into();
let requested_asset_tag = (requested_asset_id >> 52) as u8;
let payload = ((offered_asset_tag as u16) << 8) | (requested_asset_tag as u16);
let execution = NoteExecutionMode::Local;
match note_type {
NoteType::Public => NoteTag::for_public_use_case(SWAP_USE_CASE_ID, payload, execution),
_ => NoteTag::for_local_use_case(SWAP_USE_CASE_ID, payload),
}
}