diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 1bf0c15e57..f2de54dc5d 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -69,6 +69,7 @@ use tari_wallet::{ ContractDefinitionFileFormat, ContractSpecificationFileFormat, ContractUpdateProposalFileFormat, + SignatureFileFormat, }, error::WalletError, output_manager_service::handle::OutputManagerHandle, @@ -89,8 +90,10 @@ use crate::{ CliCommands, ContractCommand, ContractSubcommand, + InitAmendmentArgs, InitConstitutionArgs, InitDefinitionArgs, + InitUpdateProposalArgs, PublishFileArgs, }, utils::db::{CUSTOM_BASE_NODE_ADDRESS_KEY, CUSTOM_BASE_NODE_PUBLIC_KEY_KEY}, @@ -746,8 +749,8 @@ pub async fn command_runner( .await .map_err(CommandError::TransactionServiceError)?; }, - Contract(subcommand) => { - handle_contract_definition_command(&wallet, subcommand).await?; + Contract(command) => { + handle_contract_command(&wallet, command).await?; }, } } @@ -790,13 +793,12 @@ pub async fn command_runner( Ok(()) } -async fn handle_contract_definition_command( - wallet: &WalletSqlite, - command: ContractCommand, -) -> Result<(), CommandError> { +async fn handle_contract_command(wallet: &WalletSqlite, command: ContractCommand) -> Result<(), CommandError> { match command.subcommand { ContractSubcommand::InitDefinition(args) => init_contract_definition_spec(args), ContractSubcommand::InitConstitution(args) => init_contract_constitution_spec(args), + ContractSubcommand::InitUpdateProposal(args) => init_contract_update_proposal_spec(args), + ContractSubcommand::InitAmendment(args) => init_contract_amendment_spec(args), ContractSubcommand::PublishDefinition(args) => publish_contract_definition(wallet, args).await, ContractSubcommand::PublishConstitution(args) => publish_contract_constitution(wallet, args).await, ContractSubcommand::PublishUpdateProposal(args) => publish_contract_update_proposal(wallet, args).await, @@ -902,6 +904,132 @@ fn init_contract_constitution_spec(args: InitConstitutionArgs) -> Result<(), Com Ok(()) } +fn init_contract_update_proposal_spec(args: InitUpdateProposalArgs) -> 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_id = Prompt::new("Contract id (hex):") + .skip_if_some(args.contract_id) + .get_result()?; + let proposal_id = Prompt::new("Proposal id (integer, unique inside the contract scope):") + .skip_if_some(args.proposal_id) + .with_default("0".to_string()) + .get_result()? + .parse::() + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + let committee: Vec = Prompt::new("Validator committee ids (hex):").ask_repeatedly()?; + let acceptance_period_expiry = Prompt::new("Acceptance period expiry (in blocks, integer):") + .skip_if_some(args.acceptance_period_expiry) + .with_default("50".to_string()) + .get_result()?; + let minimum_quorum_required = Prompt::new("Minimum quorum:") + .skip_if_some(args.minimum_quorum_required) + .with_default(committee.len().to_string()) + .get_result()?; + + let updated_constitution = ConstitutionDefinitionFileFormat { + contract_id, + validator_committee: committee.iter().map(|c| PublicKey::from_hex(c).unwrap()).collect(), + consensus: SideChainConsensus::MerkleRoot, + initial_reward: 0, + acceptance_parameters: ContractAcceptanceRequirements { + acceptance_period_expiry: acceptance_period_expiry + .parse::() + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?, + minimum_quorum_required: minimum_quorum_required + .parse::() + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?, + }, + checkpoint_parameters: CheckpointParameters { + minimum_quorum_required: 0, + abandoned_interval: 0, + }, + constitution_change_rules: ConstitutionChangeRulesFileFormat { + change_flags: 0, + requirements_for_constitution_change: None, + }, + }; + + let update_proposal = ContractUpdateProposalFileFormat { + proposal_id, + // TODO: use a private key to sign the proposal + signature: SignatureFileFormat::default(), + updated_constitution, + }; + + let file = File::create(&dest).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let writer = BufWriter::new(file); + serde_json::to_writer_pretty(writer, &update_proposal).map_err(|e| CommandError::JsonFile(e.to_string()))?; + println!("Wrote {}", dest.to_string_lossy()); + Ok(()) +} + +fn init_contract_amendment_spec(args: InitAmendmentArgs) -> 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; + + // check that the proposal file exists + if !args.proposal_file_path.exists() { + println!( + "Proposal file path {} not found", + args.proposal_file_path.to_string_lossy() + ); + return Ok(()); + } + let proposal_file = File::open(&args.proposal_file_path).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let proposal_file_reader = BufReader::new(proposal_file); + + // parse the JSON file with the proposal + let update_proposal: ContractUpdateProposalFileFormat = + serde_json::from_reader(proposal_file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?; + + // read the activation_window value from the user + let activation_window = Prompt::new("Activation window (in blocks, integer):") + .skip_if_some(args.activation_window) + .with_default("50".to_string()) + .get_result()? + .parse::() + .map_err(|e| CommandError::InvalidArgument(e.to_string()))?; + + // create the amendment from the proposal + let amendment = ContractAmendmentFileFormat { + proposal_id: update_proposal.proposal_id, + validator_committee: update_proposal.updated_constitution.validator_committee.clone(), + // TODO: import the real signatures for all the proposal acceptances + validator_signatures: Vec::new(), + updated_constitution: update_proposal.updated_constitution, + activation_window, + }; + + // write the amendment to the destination file + let file = File::create(&dest).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let writer = BufWriter::new(file); + serde_json::to_writer_pretty(writer, &amendment).map_err(|e| CommandError::JsonFile(e.to_string()))?; + println!("Wrote {}", dest.to_string_lossy()); + + Ok(()) +} + async fn publish_contract_definition(wallet: &WalletSqlite, args: PublishFileArgs) -> 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()))?; diff --git a/applications/tari_console_wallet/src/cli.rs b/applications/tari_console_wallet/src/cli.rs index 4483a8ed5c..6fbb948294 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -213,6 +213,12 @@ pub enum ContractSubcommand { /// A generator for constitution files that can be edited and passed to other contract commands InitConstitution(InitConstitutionArgs), + /// A generator for update proposal files that can be edited and passed to other contract commands + InitUpdateProposal(InitUpdateProposalArgs), + + /// A generator for amendment files that can be edited and passed to other contract commands + InitAmendment(InitAmendmentArgs), + /// Creates and publishes a contract definition UTXO from the JSON spec file. PublishDefinition(PublishFileArgs), @@ -258,6 +264,42 @@ pub struct InitConstitutionArgs { pub minimum_quorum_required: Option, } +#[derive(Debug, Args, Clone)] +pub struct InitUpdateProposalArgs { + /// 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 = "id")] + pub contract_id: Option, + #[clap(long, alias = "proposal_id")] + pub proposal_id: Option, + #[clap(long, alias = "committee")] + pub validator_committee: Option>, + #[clap(long, alias = "acceptance_period")] + pub acceptance_period_expiry: Option, + #[clap(long, alias = "quorum_required")] + pub minimum_quorum_required: Option, +} + +#[derive(Debug, Args, Clone)] +pub struct InitAmendmentArgs { + /// The destination path of the contract amendment to create + pub dest_path: PathBuf, + + /// Force overwrite the destination file if it already exists + #[clap(short = 'f', long)] + pub force: bool, + + /// The source file path of the update proposal to amend + #[clap(short = 'p', long)] + pub proposal_file_path: PathBuf, + + #[clap(long, alias = "activation_window")] + pub activation_window: Option, +} + #[derive(Debug, Args, Clone)] pub struct PublishFileArgs { pub file_path: PathBuf, diff --git a/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs b/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs index ba451088d5..461138451f 100644 --- a/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs +++ b/base_layer/wallet/src/assets/contract_update_proposal_file_format.rs @@ -64,3 +64,16 @@ impl TryFrom for Signature { Ok(Signature::new(public_key, signature)) } } + +impl Default for SignatureFileFormat { + fn default() -> Self { + let default_sig = Signature::default(); + let public_nonce = default_sig.get_public_nonce().to_hex(); + let signature = default_sig.get_signature().to_hex(); + + Self { + public_nonce, + signature, + } + } +} diff --git a/base_layer/wallet/src/assets/mod.rs b/base_layer/wallet/src/assets/mod.rs index 138138f962..75cceab76f 100644 --- a/base_layer/wallet/src/assets/mod.rs +++ b/base_layer/wallet/src/assets/mod.rs @@ -39,4 +39,4 @@ mod contract_update_proposal_file_format; pub use constitution_definition_file_format::{ConstitutionChangeRulesFileFormat, ConstitutionDefinitionFileFormat}; pub use contract_amendment_file_format::ContractAmendmentFileFormat; pub use contract_definition_file_format::{ContractDefinitionFileFormat, ContractSpecificationFileFormat}; -pub use contract_update_proposal_file_format::ContractUpdateProposalFileFormat; +pub use contract_update_proposal_file_format::{ContractUpdateProposalFileFormat, SignatureFileFormat};