use crate::{helpers::TransferTypeArg, CurrentNetwork};
use aleo_rust::{
Address,
AleoAPIClient,
Ciphertext,
Encryptor,
Plaintext,
PrivateKey,
ProgramManager,
Record,
RecordFinder,
TransferType,
};
use anyhow::{anyhow, ensure, Result};
use clap::Parser;
use colored::*;
#[derive(Debug, Parser)]
pub struct Transfer {
#[clap(short, long)]
recipient: Address<CurrentNetwork>,
#[clap(short, long, value_enum, default_value_t=TransferTypeArg::Private)]
transfer_type: TransferTypeArg,
#[clap(short, long)]
amount: f64,
#[clap(short, long)]
fee: f64,
#[clap(long)]
private_fee: bool,
#[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])]
private_key: Option<PrivateKey<CurrentNetwork>>,
#[clap(long)]
amount_record: Option<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>,
#[clap(long)]
fee_record: Option<Record<CurrentNetwork, Plaintext<CurrentNetwork>>>,
#[clap(short, long)]
endpoint: Option<String>,
#[clap(short, long, conflicts_with = "private_key", requires = "password")]
ciphertext: Option<Ciphertext<CurrentNetwork>>,
#[clap(short = 'p', long, conflicts_with = "private_key", requires = "ciphertext")]
password: Option<String>,
}
impl Transfer {
pub fn parse(self) -> Result<String> {
ensure!(self.amount > 0f64, "Transfer amount must be greater than 0 credits");
ensure!(self.fee > 0f64, "fee must be greater than zero to make a transfer");
let transfer_type = TransferType::from(self.transfer_type);
ensure!(
!(self.private_key.is_none() && self.ciphertext.is_none()),
"Private key or private key ciphertext required"
);
let amount_microcredits = (self.amount * 1000000.0) as u64;
let fee_credits = self.fee;
let fee_microcredits = (fee_credits * 1000000.0) as u64;
println!(
"{}",
format!(
"Attempting to transfer {} credits to {} with a fee of {} credits...",
self.amount, self.recipient, fee_credits
)
.bright_blue()
);
let api_client = self
.endpoint
.map_or_else(
|| {
println!(
"Using default peer: {}",
"https://api.explorer.aleo.org/v1/testnet3".bright_blue().bold()
);
Ok(AleoAPIClient::<CurrentNetwork>::testnet3())
},
|peer| AleoAPIClient::<CurrentNetwork>::new(&peer, "testnet3"),
)
.map_err(|e| anyhow!("{:?}", e))?;
let program_manager = ProgramManager::<CurrentNetwork>::new(
self.private_key,
self.ciphertext.clone(),
Some(api_client.clone()),
None,
false,
)?;
let private_key = if let Some(private_key) = self.private_key {
private_key
} else {
let ciphertext = self.ciphertext.as_ref().unwrap();
Encryptor::decrypt_private_key_with_secret(ciphertext, self.password.as_ref().unwrap())?
};
let record_finder = RecordFinder::new(api_client);
let mut fee_nonce = None;
let fee_record = if self.private_fee {
let fee_record = if let Some(fee_record) = self.fee_record {
fee_record
} else {
record_finder.find_one_record(&private_key, fee_microcredits, None)?
};
Some(fee_record)
} else {
None
};
let amount_record = match transfer_type {
TransferType::Public => None,
TransferType::PublicToPrivate => None,
_ => {
if let Some(fee_record) = fee_record.as_ref() {
fee_nonce = Some([*fee_record.nonce()]);
};
if let Some(amount_record) = self.amount_record {
Some(amount_record)
} else {
Some(record_finder.find_one_record(
&private_key,
amount_microcredits,
fee_nonce.as_ref().map(|nonces| &nonces[..]),
)?)
}
}
};
let transfer = program_manager.transfer(
amount_microcredits,
fee_microcredits,
self.recipient,
transfer_type,
self.password.as_deref(),
amount_record,
fee_record,
);
if transfer.is_err() {
println!("{}", "Transfer failed with error:".to_string().red().bold());
} else {
println!("{}", "Transfer successful!".to_string().bright_green().bold());
println!("Transaction ID:");
}
transfer
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::prelude::TestRng;
#[test]
fn test_transfer_config_errors() {
let recipient_private_key = PrivateKey::<CurrentNetwork>::new(&mut TestRng::default()).unwrap();
let recipient_address = Address::<CurrentNetwork>::try_from(&recipient_private_key).unwrap();
let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap());
let transfer_missing_key_material =
Transfer::try_parse_from(["aleo", "-r", &recipient_address.to_string(), "-a", "1.0", "--fee", "0.7"]);
assert!(transfer_missing_key_material.unwrap().parse().is_err());
let transfer_conflicting_inputs = Transfer::try_parse_from([
"aleo",
"-r",
&recipient_address.to_string(),
"-a",
"2.0",
"--fee",
"0.7",
"-k",
&recipient_private_key.to_string(),
"--ciphertext",
&ciphertext.as_ref().unwrap().to_string(),
"--password",
"password",
]);
assert_eq!(transfer_conflicting_inputs.unwrap_err().kind(), clap::error::ErrorKind::ArgumentConflict);
let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap());
let transfer_no_password = Transfer::try_parse_from([
"aleo",
"-r",
&recipient_address.to_string(),
"-a",
"3.0",
"--fee",
"0.7",
"--ciphertext",
&ciphertext.as_ref().unwrap().to_string(),
]);
assert_eq!(transfer_no_password.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument);
let transfer_password_only = Transfer::try_parse_from([
"aleo",
"-r",
&recipient_address.to_string(),
"-a",
"4.0",
"--fee",
"0.7",
"--password",
"password",
]);
assert_eq!(transfer_password_only.unwrap_err().kind(), clap::error::ErrorKind::MissingRequiredArgument);
let transfer_bad_peer = Transfer::try_parse_from([
"aleo",
"-r",
&recipient_address.to_string(),
"-k",
&recipient_private_key.to_string(),
"-a",
"5.0",
"--fee",
"0.7",
"-e",
"localhost:3033",
]);
assert!(transfer_bad_peer.unwrap().parse().is_err());
let transfer_zero_amount = Transfer::try_parse_from([
"aleo",
"-r",
&recipient_address.to_string(),
"-k",
&recipient_private_key.to_string(),
"-a",
"0.0",
"--fee",
"0.7",
"-e",
"http://localhost:3033",
]);
assert!(transfer_zero_amount.unwrap().parse().is_err());
}
}