// Copyright (C) 2019-2023 Aleo Systems Inc. // This file is part of the Aleo SDK library. // The Aleo SDK library is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // The Aleo SDK library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . 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::*; /// Executes a transfer of Aleo credits #[derive(Debug, Parser)] pub struct Transfer { /// Recipient address #[clap(short, long)] recipient: Address, /// Transfer type #[clap(short, long, value_enum, default_value_t=TransferTypeArg::Private)] transfer_type: TransferTypeArg, /// Number of credits to transfer #[clap(short, long)] amount: f64, /// Transaction fee in credits #[clap(short, long)] fee: f64, #[clap(long)] /// Use private fee private_fee: bool, /// Private key used to generate the transfer #[clap(short='k', long, conflicts_with_all = &["ciphertext", "password"])] private_key: Option>, /// Record used to fund the transfer #[clap(long)] amount_record: Option>>, /// Record to spend the fee from #[clap(long)] fee_record: Option>>, /// Aleo Network peer to broadcast the transaction to #[clap(short, long)] endpoint: Option, /// Private key ciphertext used to generate the transfer (requires password to decrypt) #[clap(short, long, conflicts_with = "private_key", requires = "password")] ciphertext: Option>, /// Password to decrypt the private key #[clap(short = 'p', long, conflicts_with = "private_key", requires = "ciphertext")] password: Option, } impl Transfer { pub fn parse(self) -> Result { // Check for config errors 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" ); // Convert transfer amount and fee to microcredits 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() ); // Setup the API client to use configured peer or default to https://api.explorer.aleo.org/v1/testnet3 let api_client = self .endpoint .map_or_else( || { println!( "Using default peer: {}", "https://api.explorer.aleo.org/v1/testnet3".bright_blue().bold() ); Ok(AleoAPIClient::::testnet3()) }, |peer| AleoAPIClient::::new(&peer, "testnet3"), ) .map_err(|e| anyhow!("{:?}", e))?; // Create the program manager let program_manager = ProgramManager::::new( self.private_key, self.ciphertext.clone(), Some(api_client.clone()), None, false, )?; // Find the input records from the Aleo Network if not provided 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[..]), )?) } } }; // Execute the transfer let transfer = program_manager.transfer( amount_microcredits, fee_microcredits, self.recipient, transfer_type, self.password.as_deref(), amount_record, fee_record, ); // Inform the user of the result of the transfer 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::::new(&mut TestRng::default()).unwrap(); let recipient_address = Address::::try_from(&recipient_private_key).unwrap(); let ciphertext = Some(Encryptor::encrypt_private_key_with_secret(&recipient_private_key, "password").unwrap()); // Assert that the transfer fails without a private key or private key ciphertext 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()); // Assert transfer fails if both a private key and ciphertext are provided 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); // Assert that the transfer fails if a ciphertext is provided without a password 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); // Assert transfer fails if only a password is provided 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); // Assert transfer fails if invalid peer is specified 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()); // Assert transfer fails if a zero amount is specified 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()); } }