Skip to content

Commit

Permalink
feat: generalize TransactionRequest (#438)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomyrd authored Jul 22, 2024
1 parent 0de9d9b commit 3b85c91
Show file tree
Hide file tree
Showing 13 changed files with 500 additions and 230 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## v0.5.0 (TBD)

* [BREAKING] Refactored `TransactionRequest` to represent a generalized transaction (#438).
* Fix `get_consumable_notes` to consider block header information for consumability (#432).
* Ignored stale updates received during sync process (#412).
* Refactor `TransactionRequest` constructor (#434).
Expand Down
2 changes: 1 addition & 1 deletion bin/miden-cli/src/commands/new_transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use miden_client::{
rpc::NodeRpcClient,
store::Store,
transactions::{
transaction_request::{PaymentTransactionData, SwapTransactionData, TransactionTemplate},
request::{PaymentTransactionData, SwapTransactionData, TransactionTemplate},
TransactionResult,
},
Client,
Expand Down
48 changes: 31 additions & 17 deletions crates/rust-client/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ use miden_tx::{
};

use crate::{
notes::NoteScreenerError, rpc::RpcError, store::StoreError,
transactions::transaction_request::TransactionRequestError,
notes::NoteScreenerError,
rpc::RpcError,
store::StoreError,
transactions::{
request::TransactionRequestError, script_builder::TransactionScriptBuilderError,
},
};

// CLIENT ERROR
Expand All @@ -37,48 +41,52 @@ pub enum ClientError {
TransactionExecutorError(TransactionExecutorError),
TransactionProvingError(TransactionProverError),
TransactionRequestError(TransactionRequestError),
TransactionScriptBuilderError(TransactionScriptBuilderError),
}

impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ClientError::AccountError(err) => write!(f, "account error: {err}"),
ClientError::AssetError(err) => write!(f, "asset error: {err}"),
ClientError::AccountError(err) => write!(f, "Account error: {err}"),
ClientError::AssetError(err) => write!(f, "Asset error: {err}"),
ClientError::DataDeserializationError(err) => {
write!(f, "data deserialization error: {err}")
write!(f, "Data deserialization error: {err}")
},
ClientError::ExistenceVerificationError(note_id) => {
write!(f, "The note with ID {note_id} doesn't exist in the chain")
},
ClientError::HexParseError(err) => write!(f, "error turning array to Digest: {err}"),
ClientError::HexParseError(err) => write!(f, "Error turning array to Digest: {err}"),
ClientError::ImportNewAccountWithoutSeed => write!(
f,
"import account error: can't import a new account without its initial seed"
"Import account error: can't import a new account without its initial seed"
),
ClientError::MissingOutputNotes(note_ids) => {
write!(
f,
"transaction error: The transaction did not produce the expected notes corresponding to Note IDs: {}",
"Transaction error: The transaction did not produce the expected notes corresponding to Note IDs: {}",
note_ids.iter().map(|&id| id.to_hex()).collect::<Vec<_>>().join(", ")
)
},
ClientError::NoConsumableNoteForAccount(account_id) => {
write!(f, "No consumable note for account ID {}", account_id)
},
ClientError::NoteError(err) => write!(f, "note error: {err}"),
ClientError::NoteImportError(err) => write!(f, "error importing note: {err}"),
ClientError::NoteRecordError(err) => write!(f, "note record error: {err}"),
ClientError::RpcError(err) => write!(f, "rpc api error: {err}"),
ClientError::NoteScreenerError(err) => write!(f, "note screener error: {err}"),
ClientError::StoreError(err) => write!(f, "store error: {err}"),
ClientError::NoteError(err) => write!(f, "Note error: {err}"),
ClientError::NoteImportError(err) => write!(f, "Error importing note: {err}"),
ClientError::NoteRecordError(err) => write!(f, "Note record error: {err}"),
ClientError::RpcError(err) => write!(f, "RPC api error: {err}"),
ClientError::NoteScreenerError(err) => write!(f, "Note screener error: {err}"),
ClientError::StoreError(err) => write!(f, "Store error: {err}"),
ClientError::TransactionExecutorError(err) => {
write!(f, "transaction executor error: {err}")
write!(f, "Transaction executor error: {err}")
},
ClientError::TransactionProvingError(err) => {
write!(f, "transaction prover error: {err}")
write!(f, "Transaction prover error: {err}")
},
ClientError::TransactionRequestError(err) => {
write!(f, "transaction request error: {err}")
write!(f, "Transaction request error: {err}")
},
ClientError::TransactionScriptBuilderError(err) => {
write!(f, "Transaction script builder error: {err}")
},
}
}
Expand Down Expand Up @@ -153,6 +161,12 @@ impl From<ClientError> for String {
}
}

impl From<TransactionScriptBuilderError> for ClientError {
fn from(err: TransactionScriptBuilderError) -> Self {
Self::TransactionScriptBuilderError(err)
}
}

#[cfg(feature = "std")]
impl std::error::Error for ClientError {}

Expand Down
2 changes: 1 addition & 1 deletion crates/rust-client/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use crate::{
sync::FILTER_ID_SHIFT,
transactions::{
prepare_word,
transaction_request::{PaymentTransactionData, TransactionTemplate},
request::{PaymentTransactionData, TransactionTemplate},
},
Client,
};
Expand Down
4 changes: 2 additions & 2 deletions crates/rust-client/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::{
mock_fungible_faucet_account, mock_notes, ACCOUNT_ID_REGULAR,
},
store::{InputNoteRecord, NoteFilter},
transactions::transaction_request::TransactionTemplate,
transactions::request::TransactionTemplate,
};

#[tokio::test]
Expand Down Expand Up @@ -394,8 +394,8 @@ async fn test_mint_transaction() {
);

let transaction_request = client.build_transaction_request(transaction_template).unwrap();

let transaction = client.new_transaction(transaction_request).unwrap();

assert!(transaction.executed_transaction().account_delta().nonce().is_some());
}

Expand Down
165 changes: 72 additions & 93 deletions crates/rust-client/src/transactions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ use core::fmt;

use miden_lib::notes::{create_p2id_note, create_p2idr_note, create_swap_note};
use miden_objects::{
accounts::{AccountDelta, AccountId},
accounts::{AccountDelta, AccountId, AccountType},
assembly::ProgramAst,
assets::FungibleAsset,
notes::{Note, NoteDetails, NoteId, NoteType},
transaction::{InputNotes, TransactionArgs},
Digest, Felt, FieldElement, Word,
};
use miden_tx::{auth::TransactionAuthenticator, ProvingOptions, TransactionProver};
use request::{TransactionRequestError, TransactionScriptTemplate};
use script_builder::{AccountCapabilities, AccountInterface, TransactionScriptBuilder};
use tracing::info;
use transaction_request::TransactionRequestError;
use winter_maybe_async::{maybe_async, maybe_await};

use self::transaction_request::{
use self::request::{
PaymentTransactionData, SwapTransactionData, TransactionRequest, TransactionTemplate,
};
use super::{rpc::NodeRpcClient, Client, FeltRng};
Expand All @@ -29,13 +30,14 @@ use crate::{
ClientError,
};

pub mod transaction_request;
pub mod request;
pub mod script_builder;
pub use miden_objects::transaction::{
ExecutedTransaction, InputNote, OutputNote, OutputNotes, ProvenTransaction, TransactionId,
TransactionScript,
};
pub use miden_tx::{DataStoreError, ScriptTarget, TransactionExecutorError};
pub use transaction_request::known_script_roots;
pub use request::known_script_roots;

// TRANSACTION RESULT
// --------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -199,15 +201,10 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
) -> Result<TransactionRequest, ClientError> {
match transaction_template {
TransactionTemplate::ConsumeNotes(account_id, notes) => {
let program_ast = ProgramAst::parse(transaction_request::AUTH_CONSUME_NOTES_SCRIPT)
.expect("shipped MASM is well-formed");
let notes = notes.iter().map(|id| (*id, None)).collect();
let notes = notes.iter().map(|id| (*id, None));

let tx_script = self.tx_executor.compile_tx_script(program_ast, vec![], vec![])?;

let tx_request = TransactionRequest::new(account_id)
.with_authenticated_input_notes(notes)
.with_tx_script(tx_script);
let tx_request =
TransactionRequest::new(account_id).with_authenticated_input_notes(notes);

Ok(tx_request)
},
Expand All @@ -226,14 +223,15 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
}
}

/// Creates and executes a transaction specified by the template, but does not change the
/// Creates and executes a transaction specified by the request, but does not change the
/// local database.
///
/// # Errors
///
/// - Returns [ClientError::MissingOutputNotes] if the [TransactionRequest] ouput notes are
/// not a subset of executor's output notes
/// - Returns a [ClientError::TransactionExecutorError] if the execution fails
/// - Returns a [ClientError::TransactionRequestError] if the request is invalid
#[maybe_async]
pub fn new_transaction(
&mut self,
Expand Down Expand Up @@ -270,16 +268,41 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
let block_num = maybe_await!(self.store.get_sync_height())?;

let note_ids = transaction_request.get_input_note_ids();
let output_notes = transaction_request.expected_output_notes().to_vec();
let future_notes = transaction_request.expected_future_notes().to_vec();
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(&self.tx_executor, 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(&self.tx_executor)?
},
};

let tx_args = transaction_request.into_transaction_args(tx_script);

// Execute the transaction and get the witness
let executed_transaction = maybe_await!(self.tx_executor.execute_transaction(
account_id,
block_num,
&note_ids,
transaction_request.into(),
))?;
let executed_transaction = maybe_await!(self
.tx_executor
.execute_transaction(account_id, block_num, &note_ids, tx_args,))?;

// Check that the expected output notes matches the transaction outcome.
// We compare authentication hashes where possible since that involves note IDs + metadata
Expand Down Expand Up @@ -383,31 +406,8 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
)?
};

let recipient = created_note
.recipient()
.digest()
.iter()
.map(|x| x.as_int().to_string())
.collect::<Vec<_>>()
.join(".");

let note_tag = created_note.metadata().tag().inner();

let tx_script = ProgramAst::parse(
&transaction_request::AUTH_SEND_ASSET_SCRIPT
.replace("{recipient}", &recipient)
.replace("{note_type}", &Felt::new(note_type as u64).to_string())
.replace("{aux}", &created_note.metadata().aux().to_string())
.replace("{tag}", &Felt::new(note_tag.into()).to_string())
.replace("{asset}", &prepare_word(&payment_data.asset().into()).to_string()),
)
.expect("shipped MASM is well-formed");

let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?;

Ok(TransactionRequest::new(payment_data.account_id())
.with_expected_output_notes(vec![created_note])
.with_tx_script(tx_script))
.with_own_output_notes(vec![OutputNote::Full(created_note)])?)
}

/// Helper to build a [TransactionRequest] for Swap-type transactions easily.
Expand All @@ -429,32 +429,9 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
&mut self.rng,
)?;

let recipient = created_note
.recipient()
.digest()
.iter()
.map(|x| x.as_int().to_string())
.collect::<Vec<_>>()
.join(".");

let note_tag = created_note.metadata().tag().inner();

let tx_script = ProgramAst::parse(
&transaction_request::AUTH_SEND_ASSET_SCRIPT
.replace("{recipient}", &recipient)
.replace("{note_type}", &Felt::new(note_type as u64).to_string())
.replace("{aux}", &created_note.metadata().aux().to_string())
.replace("{tag}", &Felt::new(note_tag.into()).to_string())
.replace("{asset}", &prepare_word(&swap_data.offered_asset().into()).to_string()),
)
.expect("shipped MASM is well-formed");

let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?;

Ok(TransactionRequest::new(swap_data.account_id())
.with_expected_output_notes(vec![created_note])
.with_expected_future_notes(vec![payback_note_details])
.with_tx_script(tx_script))
.with_own_output_notes(vec![OutputNote::Full(created_note)])?)
}

/// Helper to build a [TransactionRequest] for transaction to mint fungible tokens.
Expand All @@ -475,31 +452,33 @@ impl<N: NodeRpcClient, R: FeltRng, S: Store, A: TransactionAuthenticator> Client
&mut self.rng,
)?;

let recipient = created_note
.recipient()
.digest()
.iter()
.map(|x| x.as_int().to_string())
.collect::<Vec<_>>()
.join(".");

let note_tag = created_note.metadata().tag().inner();

let tx_script = ProgramAst::parse(
&transaction_request::DISTRIBUTE_FUNGIBLE_ASSET_SCRIPT
.replace("{recipient}", &recipient)
.replace("{note_type}", &Felt::new(note_type as u64).to_string())
.replace("{aux}", &created_note.metadata().aux().to_string())
.replace("{tag}", &Felt::new(note_tag.into()).to_string())
.replace("{amount}", &Felt::new(asset.amount()).to_string()),
)
.expect("shipped MASM is well-formed");
Ok(TransactionRequest::new(asset.faucet_id())
.with_own_output_notes(vec![OutputNote::Full(created_note)])?)
}

let tx_script = self.tx_executor.compile_tx_script(tx_script, vec![], vec![])?;
/// Retrieves the account capabilities for the specified 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))?;

// TODO: we should check if the account actually exposes the interfaces we're trying to use
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(TransactionRequest::new(asset.faucet_id())
.with_expected_output_notes(vec![created_note])
.with_tx_script(tx_script))
Ok(AccountCapabilities {
account_id,
auth: account_auth,
interfaces: account_capabilities,
})
}
}

Expand Down
Loading

0 comments on commit 3b85c91

Please sign in to comment.