diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index c8e8600077..5cc60890b6 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -22,7 +22,7 @@ use std::{ fs::File, - io::{BufReader, LineWriter, Write}, + io::{BufReader, BufWriter, LineWriter, Write}, path::PathBuf, time::{Duration, Instant}, }; @@ -47,7 +47,7 @@ use tari_core::transactions::{ use tari_crypto::ristretto::pedersen::PedersenCommitmentFactory; use tari_utilities::{hex::Hex, ByteArray, Hashable}; use tari_wallet::{ - assets::ContractDefinitionFileFormat, + assets::{ContractDefinitionFileFormat, ContractSpecificationFileFormat}, error::WalletError, output_manager_service::handle::OutputManagerHandle, transaction_service::handle::{TransactionEvent, TransactionServiceHandle}, @@ -62,7 +62,14 @@ use tokio::{ use super::error::CommandError; use crate::{ - cli::CliCommands, + automation::prompt::{HexArg, Prompt}, + cli::{ + CliCommands, + ContractDefinitionCommand, + ContractDefinitionSubcommand, + InitContractDefinitionArgs, + PublishContractDefinitionArgs, + }, utils::db::{CUSTOM_BASE_NODE_ADDRESS_KEY, CUSTOM_BASE_NODE_PUBLIC_KEY_KEY}, }; @@ -719,34 +726,8 @@ pub async fn command_runner( .await .map_err(CommandError::TransactionServiceError)?; }, - PublishContractDefinition(args) => { - // open the JSON file with the contract definition values - let file = File::open(&args.file_path).map_err(|e| CommandError::JSONFile(e.to_string()))?; - let file_reader = BufReader::new(file); - - // parse the JSON file - let contract_definition: ContractDefinitionFileFormat = - serde_json::from_reader(file_reader).map_err(|e| CommandError::JSONFile(e.to_string()))?; - let contract_definition_features = ContractDefinition::from(contract_definition); - let contract_id_hex = contract_definition_features.calculate_contract_id().to_vec().to_hex(); - - // create the contract definition transaction - let mut asset_manager = wallet.asset_manager.clone(); - let (tx_id, transaction) = asset_manager - .create_contract_definition(&contract_definition_features) - .await?; - - // publish the contract definition transaction - let message = format!("Contract definition for contract with id={}", contract_id_hex); - transaction_service - .submit_transaction(tx_id, transaction, 0.into(), message) - .await?; - - println!( - "Contract definition transaction submitted with tx_id={} for contract with contract_id={}", - tx_id, contract_id_hex - ); - println!("Done!"); + ContractDefinition(subcommand) => { + handle_contract_definition_command(&wallet, subcommand).await?; }, } } @@ -789,6 +770,92 @@ pub async fn command_runner( Ok(()) } +async fn handle_contract_definition_command( + wallet: &WalletSqlite, + command: ContractDefinitionCommand, +) -> Result<(), CommandError> { + match command.subcommand { + ContractDefinitionSubcommand::Init(args) => init_contract_definition_spec(args), + ContractDefinitionSubcommand::Publish(args) => publish_contract_definition(wallet, args).await, + } +} + +fn init_contract_definition_spec(args: InitContractDefinitionArgs) -> Result<(), CommandError> { + if args.dest_path.exists() { + if args.force { + println!("{} exists and will be overwritten.", args.dest_path.to_string_lossy()); + } else { + println!( + "{} exists. Use `--force` to overwrite.", + args.dest_path.to_string_lossy() + ); + return Ok(()); + } + } + let dest = args.dest_path; + + let contract_name = Prompt::new("Contract name (max 32 characters):") + .skip_if_some(args.contract_name) + .get_result()?; + let contract_issuer = Prompt::new("Issuer public Key (hex):") + .skip_if_some(args.contract_issuer) + .get_result_parsed::>()?; + let runtime = Prompt::new("Contract runtime:") + .skip_if_some(args.runtime) + .with_default("/tari/wasm/v0.1".to_string()) + .get_result()?; + + let contract_definition = ContractDefinitionFileFormat { + contract_name, + contract_issuer: contract_issuer.into_inner(), + contract_spec: ContractSpecificationFileFormat { + runtime, + public_functions: vec![], + }, + }; + + let file = File::create(&dest).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let writer = BufWriter::new(file); + serde_json::to_writer_pretty(writer, &contract_definition).map_err(|e| CommandError::JsonFile(e.to_string()))?; + println!("Wrote {}", dest.to_string_lossy()); + Ok(()) +} + +async fn publish_contract_definition( + wallet: &WalletSqlite, + args: PublishContractDefinitionArgs, +) -> Result<(), CommandError> { + // open the JSON file with the contract definition values + let file = File::open(&args.file_path).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let file_reader = BufReader::new(file); + + // parse the JSON file + let contract_definition: ContractDefinitionFileFormat = + serde_json::from_reader(file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let contract_definition_features = ContractDefinition::from(contract_definition); + let contract_id_hex = contract_definition_features.calculate_contract_id().to_vec().to_hex(); + + // create the contract definition transaction + let mut asset_manager = wallet.asset_manager.clone(); + let (tx_id, transaction) = asset_manager + .create_contract_definition(&contract_definition_features) + .await?; + + // publish the contract definition transaction + let message = format!("Contract definition for contract with id={}", contract_id_hex); + let mut transaction_service = wallet.transaction_service.clone(); + transaction_service + .submit_transaction(tx_id, transaction, 0.into(), message) + .await?; + + println!( + "Contract definition transaction submitted with tx_id={} for contract with contract_id={}", + tx_id, contract_id_hex + ); + println!("Done!"); + Ok(()) +} + fn write_utxos_to_csv_file(utxos: Vec, file_path: PathBuf) -> Result<(), CommandError> { let factory = PedersenCommitmentFactory::default(); let file = File::create(file_path).map_err(|e| CommandError::CSVFile(e.to_string()))?; diff --git a/applications/tari_console_wallet/src/automation/error.rs b/applications/tari_console_wallet/src/automation/error.rs index 2bb1d2f533..54b3a33367 100644 --- a/applications/tari_console_wallet/src/automation/error.rs +++ b/applications/tari_console_wallet/src/automation/error.rs @@ -20,7 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::num::{ParseFloatError, ParseIntError}; +use std::{ + io, + num::{ParseFloatError, ParseIntError}, +}; use log::*; use tari_common::exit_codes::{ExitCode, ExitError}; @@ -42,6 +45,8 @@ pub const LOG_TARGET: &str = "wallet::automation::error"; pub enum CommandError { #[error("Argument error - were they in the right order?")] Argument, + #[error("Invalid argument: {0}")] + InvalidArgument(String), #[error("Tari value error `{0}`")] MicroTariError(#[from] MicroTariError), #[error("Transaction service error `{0}`")] @@ -69,7 +74,9 @@ pub enum CommandError { #[error("Error `{0}`")] ShaError(String), #[error("JSON file error `{0}`")] - JSONFile(String), + JsonFile(String), + #[error(transparent)] + IoError(#[from] io::Error), } impl From for ExitError { diff --git a/applications/tari_console_wallet/src/automation/mod.rs b/applications/tari_console_wallet/src/automation/mod.rs index dd897852e7..a1a88d48af 100644 --- a/applications/tari_console_wallet/src/automation/mod.rs +++ b/applications/tari_console_wallet/src/automation/mod.rs @@ -22,3 +22,4 @@ pub mod commands; pub mod error; +mod prompt; diff --git a/applications/tari_console_wallet/src/automation/prompt.rs b/applications/tari_console_wallet/src/automation/prompt.rs new file mode 100644 index 0000000000..d41b236454 --- /dev/null +++ b/applications/tari_console_wallet/src/automation/prompt.rs @@ -0,0 +1,111 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{io, io::Write, str::FromStr}; + +use tari_utilities::hex::{Hex, HexError}; + +use crate::automation::error::CommandError; + +pub struct Prompt<'a> { + label: &'a str, + skip_if_some: Option, + default: Option, +} + +impl<'a> Prompt<'a> { + pub fn new(label: &'a str) -> Self { + Self { + label, + default: None, + skip_if_some: None, + } + } + + pub fn skip_if_some(mut self, value: Option) -> Self { + self.skip_if_some = value; + self + } + + pub fn with_default(mut self, default: String) -> Self { + self.default = Some(default); + self + } + + pub fn get_result_parsed(self) -> Result + where + T: FromStr, + T::Err: ToString, + { + let result = self.get_result()?; + let parsed = result + .parse() + .map_err(|e: T::Err| CommandError::InvalidArgument(e.to_string()))?; + Ok(parsed) + } + + pub fn get_result(self) -> Result { + if let Some(value) = self.skip_if_some { + return Ok(value); + } + loop { + match self.default { + Some(ref default) => { + println!("{} (Default: {})", self.label, default); + }, + None => { + println!("{}", self.label); + }, + } + print!("> "); + io::stdout().flush()?; + let mut line_buf = String::new(); + io::stdin().read_line(&mut line_buf)?; + println!(); + let trimmed = line_buf.trim(); + if trimmed.is_empty() { + match self.default { + Some(ref default) => return Ok(default.clone()), + None => continue, + } + } else { + return Ok(trimmed.to_string()); + } + } + } +} + +pub struct HexArg(T); + +impl HexArg { + pub fn into_inner(self) -> T { + self.0 + } +} + +impl FromStr for HexArg { + type Err = HexError; + + fn from_str(s: &str) -> Result { + Ok(Self(T::from_hex(s)?)) + } +} diff --git a/applications/tari_console_wallet/src/cli.rs b/applications/tari_console_wallet/src/cli.rs index 6a023cd59d..1f402fc311 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -109,7 +109,7 @@ pub enum CliCommands { FinaliseShaAtomicSwap(FinaliseShaAtomicSwapArgs), ClaimShaAtomicSwapRefund(ClaimShaAtomicSwapRefundArgs), RevalidateWalletDb, - PublishContractDefinition(PublishContractDefinitionArgs), + ContractDefinition(ContractDefinitionCommand), } #[derive(Debug, Args, Clone)] @@ -192,12 +192,42 @@ fn parse_hex(s: &str) -> Result, HexError> { #[derive(Debug, Args, Clone)] pub struct ClaimShaAtomicSwapRefundArgs { - #[clap(short, long, parse(try_from_str = parse_hex), required=true )] + #[clap(short, long, parse(try_from_str = parse_hex), required = true)] pub output_hash: Vec>, #[clap(short, long, default_value = "Claimed HTLC atomic swap refund")] pub message: String, } +#[derive(Debug, Args, Clone)] +pub struct ContractDefinitionCommand { + #[clap(subcommand)] + pub subcommand: ContractDefinitionSubcommand, +} + +#[derive(Debug, Subcommand, Clone)] +pub enum ContractDefinitionSubcommand { + /// Generates a new contract definition JSON spec file that can be edited and passed to other contract definition + /// commands. + Init(InitContractDefinitionArgs), + /// Creates and publishes a contract definition UTXO from the JSON spec file. + Publish(PublishContractDefinitionArgs), +} + +#[derive(Debug, Args, Clone)] +pub struct InitContractDefinitionArgs { + /// The destination path of the contract definition to create + pub dest_path: PathBuf, + /// Force overwrite the destination file if it already exists + #[clap(short = 'f', long)] + pub force: bool, + #[clap(long, alias = "name")] + pub contract_name: Option, + #[clap(long, alias = "issuer")] + pub contract_issuer: Option, + #[clap(long, alias = "runtime")] + pub runtime: Option, +} + #[derive(Debug, Args, Clone)] pub struct PublishContractDefinitionArgs { pub file_path: PathBuf, diff --git a/base_layer/wallet/src/assets/mod.rs b/base_layer/wallet/src/assets/mod.rs index 0d75edee01..6546022de7 100644 --- a/base_layer/wallet/src/assets/mod.rs +++ b/base_layer/wallet/src/assets/mod.rs @@ -32,4 +32,4 @@ pub use asset_manager_handle::AssetManagerHandle; pub(crate) mod infrastructure; mod contract_definition_file_format; -pub use contract_definition_file_format::ContractDefinitionFileFormat; +pub use contract_definition_file_format::{ContractDefinitionFileFormat, ContractSpecificationFileFormat};