use std::io;
use std::sync::Arc;
use clap::{Parser, ValueEnum};
use miden_client::account::AccountId;
use miden_client::asset::{FungibleAsset, NonFungibleDeltaAction};
use miden_client::keystore::Keystore;
use miden_client::note::{
BlockNumber,
NoteType as MidenNoteType,
SwapNote,
get_input_note_with_id_prefix,
};
use miden_client::store::NoteRecordError;
use miden_client::transaction::{
ExecutedTransaction,
InputNote,
PaymentNoteDescription,
RawOutputNote,
SwapTransactionData,
TransactionRequest,
TransactionRequestBuilder,
};
use miden_client::{Client, RemoteTransactionProver};
use tracing::info;
use crate::config::CliConfig;
use crate::create_dynamic_table;
use crate::errors::CliError;
use crate::utils::{
SHARED_TOKEN_DOCUMENTATION,
get_input_acc_id_by_prefix_or_default,
load_faucet_details_map,
parse_account_id,
};
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum NoteType {
Public,
Private,
}
impl From<&NoteType> for MidenNoteType {
fn from(note_type: &NoteType) -> Self {
match note_type {
NoteType::Public => MidenNoteType::Public,
NoteType::Private => MidenNoteType::Private,
}
}
}
#[derive(Debug, Parser, Clone)]
pub struct MintCmd {
#[arg(short = 't', long = "target")]
target_account_id: String,
#[arg(short, long, help=format!("Asset to be minted.\n{SHARED_TOKEN_DOCUMENTATION}"))]
asset: String,
#[arg(short, long, value_enum)]
note_type: NoteType,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false)]
delegate_proving: bool,
}
impl MintCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
) -> Result<(), CliError> {
let force = self.force;
let faucet_details_map = load_faucet_details_map()?;
let fungible_asset = faucet_details_map.parse_fungible_asset(&client, &self.asset).await?;
let target_account_id = parse_account_id(&client, self.target_account_id.as_str()).await?;
let transaction_request = TransactionRequestBuilder::new()
.build_mint_fungible_asset(
fungible_asset,
target_account_id,
(&self.note_type).into(),
client.rng(),
)
.map_err(|err| {
CliError::Transaction(err.into(), "Failed to build mint transaction".to_string())
})?;
execute_transaction(
&mut client,
fungible_asset.faucet_id(),
transaction_request,
force,
self.delegate_proving,
)
.await
}
}
#[derive(Debug, Parser, Clone)]
pub struct SendCmd {
#[arg(short = 's', long = "sender")]
sender_account_id: Option<String>,
#[arg(short = 't', long = "target")]
target_account_id: String,
#[arg(short, long, help=format!("Asset to be sent.\n{SHARED_TOKEN_DOCUMENTATION}"))]
asset: String,
#[arg(short, long, value_enum)]
note_type: NoteType,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(short, long)]
recall_height: Option<u32>,
#[arg(short = 'i', long)]
timelock_height: Option<u32>,
#[arg(long, default_value_t = false)]
delegate_proving: bool,
}
impl SendCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
) -> Result<(), CliError> {
let force = self.force;
let faucet_details_map = load_faucet_details_map()?;
let fungible_asset = faucet_details_map.parse_fungible_asset(&client, &self.asset).await?;
let sender_account_id =
get_input_acc_id_by_prefix_or_default(&client, self.sender_account_id.clone()).await?;
let target_account_id = parse_account_id(&client, self.target_account_id.as_str()).await?;
let mut payment_description = PaymentNoteDescription::new(
vec![fungible_asset.into()],
sender_account_id,
target_account_id,
);
if let Some(recall_height) = self.recall_height {
payment_description =
payment_description.with_reclaim_height(BlockNumber::from(recall_height));
}
if let Some(timelock_height) = self.timelock_height {
payment_description =
payment_description.with_timelock_height(BlockNumber::from(timelock_height));
}
let transaction_request = TransactionRequestBuilder::new()
.build_pay_to_id(payment_description, (&self.note_type).into(), client.rng())
.map_err(|err| {
CliError::Transaction(err.into(), "Failed to build payment transaction".to_string())
})?;
execute_transaction(
&mut client,
sender_account_id,
transaction_request,
force,
self.delegate_proving,
)
.await
}
}
#[derive(Debug, Parser, Clone)]
pub struct SwapCmd {
#[arg(short = 's', long = "source")]
sender_account_id: Option<String>,
#[arg(short = 'o', long = "offered-asset", help=format!("Asset offered.\n{SHARED_TOKEN_DOCUMENTATION}"))]
offered_asset: String,
#[arg(short, long, help=format!("Asset requested.\n{SHARED_TOKEN_DOCUMENTATION}"))]
requested_asset: String,
#[arg(short, long, value_enum)]
note_type: NoteType,
#[arg(long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false)]
delegate_proving: bool,
}
impl SwapCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
) -> Result<(), CliError> {
let force = self.force;
let faucet_details_map = load_faucet_details_map()?;
let offered_fungible_asset =
faucet_details_map.parse_fungible_asset(&client, &self.offered_asset).await?;
let requested_fungible_asset =
faucet_details_map.parse_fungible_asset(&client, &self.requested_asset).await?;
let sender_account_id =
get_input_acc_id_by_prefix_or_default(&client, self.sender_account_id.clone()).await?;
let swap_transaction = SwapTransactionData::new(
sender_account_id,
offered_fungible_asset.into(),
requested_fungible_asset.into(),
);
let transaction_request = TransactionRequestBuilder::new()
.build_swap(
&swap_transaction,
(&self.note_type).into(),
MidenNoteType::Private,
client.rng(),
)
.map_err(|err| {
CliError::Transaction(err.into(), "Failed to build swap transaction".to_string())
})?;
execute_transaction(
&mut client,
sender_account_id,
transaction_request,
force,
self.delegate_proving,
)
.await?;
let payback_note_tag: u32 = SwapNote::build_tag(
(&self.note_type).into(),
&swap_transaction.offered_asset(),
&swap_transaction.requested_asset(),
)
.into();
println!(
"To receive updates about the payback Swap Note run `miden-client tags --add {payback_note_tag}`",
);
Ok(())
}
}
#[derive(Debug, Parser, Clone)]
pub struct ConsumeNotesCmd {
#[arg(short = 'a', long = "account")]
account_id: Option<String>,
list_of_notes: Vec<String>,
#[arg(short, long, default_value_t = false)]
force: bool,
#[arg(long, default_value_t = false)]
delegate_proving: bool,
}
impl ConsumeNotesCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
) -> Result<(), CliError> {
let force = self.force;
let mut input_notes = Vec::new();
for note_id in &self.list_of_notes {
let note_record = get_input_note_with_id_prefix(&client, note_id)
.await
.map_err(|_| CliError::Input(format!("Input note ID {note_id} is neither a valid Note ID nor a prefix of a known Note ID")))?;
input_notes.push((
note_record.try_into().map_err(|err: NoteRecordError| {
CliError::Transaction(err.into(), "Failed to convert note record".to_string())
})?,
None,
));
}
let account_id =
get_input_acc_id_by_prefix_or_default(&client, self.account_id.clone()).await?;
if input_notes.is_empty() {
info!("No input note IDs provided, getting all notes consumable by {}", account_id);
let consumable_notes = client.get_consumable_notes(Some(account_id)).await?;
for (note_record, _) in consumable_notes {
input_notes.push((
note_record.try_into().map_err(|err: NoteRecordError| {
CliError::Transaction(
err.into(),
"Failed to convert note record".to_string(),
)
})?,
None,
));
}
}
if input_notes.is_empty() {
return Err(CliError::Transaction(
"No input notes were provided and the store does not contain any notes consumable by {account_id}".into(),
"Input notes check failed".to_string(),
));
}
let transaction_request = TransactionRequestBuilder::new()
.input_notes(input_notes)
.build()
.map_err(|err| {
CliError::Transaction(
err.into(),
"Failed to build consume notes transaction".to_string(),
)
})?;
execute_transaction(
&mut client,
account_id,
transaction_request,
force,
self.delegate_proving,
)
.await
}
}
async fn execute_transaction<AUTH: Keystore + Sync + 'static>(
client: &mut Client<AUTH>,
account_id: AccountId,
transaction_request: TransactionRequest,
force: bool,
delegated_proving: bool,
) -> Result<(), CliError> {
println!("Executing transaction...");
let transaction_result = client.execute_transaction(account_id, transaction_request).await?;
let executed_transaction = transaction_result.executed_transaction().clone();
print_transaction_details(&executed_transaction)?;
if !force {
println!(
"\nContinue with proving and submission? Changes will be irreversible once the proof is finalized on the network (y/N)"
);
let mut proceed_str: String = String::new();
io::stdin().read_line(&mut proceed_str).expect("Should read line");
if proceed_str.trim().to_lowercase() != "y" {
println!("Transaction was cancelled.");
return Ok(());
}
}
let transaction_id = executed_transaction.id();
let output_notes = executed_transaction
.output_notes()
.iter()
.map(RawOutputNote::id)
.collect::<Vec<_>>();
println!("Proving transaction...");
let prover = if delegated_proving {
let cli_config = CliConfig::load()?;
let remote_prover_endpoint =
cli_config.remote_prover_endpoint.as_ref().ok_or(CliError::Config(
"Remote prover endpoint".to_string().into(),
"remote prover endpoint is not set in the configuration file".to_string(),
))?;
Arc::new(
RemoteTransactionProver::new(remote_prover_endpoint.to_string())
.with_timeout(cli_config.remote_prover_timeout),
)
} else {
client.prover()
};
let proven_transaction = client.prove_transaction_with(&transaction_result, prover).await?;
println!("Submitting transaction to node...");
let submission_height = client
.submit_proven_transaction(proven_transaction, &transaction_result)
.await?;
println!("Applying transaction to store...");
client.apply_transaction(&transaction_result, submission_height).await?;
println!("Successfully created transaction.");
println!("Transaction ID: {transaction_id}");
if output_notes.is_empty() {
println!("The transaction did not generate any output notes.");
} else {
println!("Output notes:");
for note_id in &output_notes {
println!("\t- {note_id}");
}
}
Ok(())
}
fn print_transaction_details(executed_tx: &ExecutedTransaction) -> Result<(), CliError> {
println!("The transaction will have the following effects:\n");
let input_note_ids = executed_tx.input_notes().iter().map(InputNote::id).collect::<Vec<_>>();
if input_note_ids.is_empty() {
println!("No notes will be consumed.");
} else {
println!("The following notes will be consumed:");
for input_note_id in input_note_ids {
println!("\t- {}", input_note_id.to_hex());
}
}
println!();
let output_note_count = executed_tx.output_notes().iter().count();
if output_note_count == 0 {
println!("No notes will be created as a result of this transaction.");
} else {
println!("{output_note_count} notes will be created as a result of this transaction.");
}
println!();
println!("The account with ID {} will be modified as follows:", executed_tx.account_id());
let account_delta = executed_tx.account_delta();
let has_storage_changes = !account_delta.storage().is_empty();
if has_storage_changes {
let mut table = create_dynamic_table(&["Storage Slot", "Effect"]);
for (updated_item_slot, new_value) in account_delta.storage().values() {
table.add_row(vec![
updated_item_slot.to_string(),
format!("Updated ({})", new_value.to_hex()),
]);
}
println!("Storage changes:");
println!("{table}");
} else {
println!("Account Storage will not be changed.");
}
if account_delta.vault().is_empty() {
println!("Account Vault will not be changed.");
} else {
let faucet_details_map = load_faucet_details_map()?;
let mut table = create_dynamic_table(&["Asset Type", "Faucet ID", "Amount"]);
for (vault_key, amount) in account_delta.vault().fungible().iter() {
let asset = FungibleAsset::new(vault_key.faucet_id(), amount.unsigned_abs())
.map_err(CliError::Asset)?;
let (faucet_fmt, amount_fmt) = faucet_details_map.format_fungible_asset(&asset)?;
if amount.is_positive() {
table.add_row(vec!["Fungible Asset", &faucet_fmt, &format!("+{amount_fmt}")]);
} else {
table.add_row(vec!["Fungible Asset", &faucet_fmt, &format!("-{amount_fmt}")]);
}
}
for (asset, action) in account_delta.vault().non_fungible().iter() {
match action {
NonFungibleDeltaAction::Add => {
table.add_row(vec![
"Non Fungible Asset",
&asset.faucet_id().prefix().to_hex(),
"1",
]);
},
NonFungibleDeltaAction::Remove => {
table.add_row(vec![
"Non Fungible Asset",
&asset.faucet_id().prefix().to_hex(),
"-1",
]);
},
}
}
println!("Vault changes:");
println!("{table}");
}
println!("Nonce incremented by: {}.", account_delta.nonce_delta());
Ok(())
}