diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 3b6dfb3357..1bf0c15e57 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -50,6 +50,7 @@ use tari_core::transactions::{ transaction_components::{ CheckpointParameters, ContractAcceptanceRequirements, + ContractAmendment, ContractDefinition, ContractUpdateProposal, SideChainConsensus, @@ -64,6 +65,7 @@ use tari_wallet::{ assets::{ ConstitutionChangeRulesFileFormat, ConstitutionDefinitionFileFormat, + ContractAmendmentFileFormat, ContractDefinitionFileFormat, ContractSpecificationFileFormat, ContractUpdateProposalFileFormat, @@ -798,6 +800,7 @@ async fn handle_contract_definition_command( 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, + ContractSubcommand::PublishAmendment(args) => publish_contract_amendment(wallet, args).await, } } @@ -992,6 +995,40 @@ async fn publish_contract_update_proposal(wallet: &WalletSqlite, args: PublishFi Ok(()) } +async fn publish_contract_amendment(wallet: &WalletSqlite, args: PublishFileArgs) -> Result<(), CommandError> { + 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 amendment: ContractAmendmentFileFormat = + serde_json::from_reader(file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let contract_id_hex = amendment.updated_constitution.contract_id.clone(); + let contract_id = FixedHash::from_hex(&contract_id_hex).map_err(|e| CommandError::JsonFile(e.to_string()))?; + let amendment_features = ContractAmendment::try_from(amendment).map_err(CommandError::JsonFile)?; + + let mut asset_manager = wallet.asset_manager.clone(); + let (tx_id, transaction) = asset_manager + .create_contract_amendment(&contract_id, &amendment_features) + .await?; + + let message = format!( + "Contract amendment {} for contract {}", + amendment_features.proposal_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 amendment transaction submitted with tx_id={} for contract with contract_id={}", + tx_id, contract_id_hex + ); + + 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/cli.rs b/applications/tari_console_wallet/src/cli.rs index fa58c189f7..4483a8ed5c 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -221,6 +221,9 @@ pub enum ContractSubcommand { /// Creates and publishes a contract update proposal UTXO from the JSON spec file. PublishUpdateProposal(PublishFileArgs), + + /// Creates and publishes a contract amendment UTXO from the JSON spec file. + PublishAmendment(PublishFileArgs), } #[derive(Debug, Args, Clone)] diff --git a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs index 3c581383c9..9d5661aa38 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/contract_index.rs @@ -174,7 +174,8 @@ impl<'a> ContractIndex<'a, WriteTransaction<'a>> { // These are collections of output hashes OutputType::ContractValidatorAcceptance | OutputType::ContractConstitutionProposal | - OutputType::ContractConstitutionChangeAcceptance => { + OutputType::ContractConstitutionChangeAcceptance | + OutputType::ContractAmendment => { self.assert_definition_exists(contract_id)?; let mut hashes = self.find::(&key)?.unwrap_or_default(); diff --git a/base_layer/core/src/transactions/transaction_components/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs index 7fd04cb775..a5cd0df48b 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -39,6 +39,7 @@ use tari_utilities::ByteArray; use super::{ ContractAcceptance, + ContractAmendment, ContractDefinition, ContractUpdateProposal, ContractUpdateProposalAcceptance, @@ -354,6 +355,18 @@ impl OutputFeatures { } } + pub fn for_contract_amendment(contract_id: FixedHash, amendment: ContractAmendment) -> OutputFeatures { + Self { + output_type: OutputType::ContractAmendment, + sidechain_features: Some( + SideChainFeaturesBuilder::new(contract_id) + .with_contract_amendment(amendment) + .finish(), + ), + ..Default::default() + } + } + pub fn unique_asset_id(&self) -> Option<&[u8]> { self.unique_id.as_deref() } diff --git a/base_layer/core/src/transactions/transaction_components/output_type.rs b/base_layer/core/src/transactions/transaction_components/output_type.rs index abbc712272..86351ab875 100644 --- a/base_layer/core/src/transactions/transaction_components/output_type.rs +++ b/base_layer/core/src/transactions/transaction_components/output_type.rs @@ -29,11 +29,13 @@ use std::{ io::Read, }; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; use serde_repr::{Deserialize_repr, Serialize_repr}; use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; -#[derive(Debug, Clone, Copy, Hash, Deserialize_repr, Serialize_repr, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Hash, Deserialize_repr, Serialize_repr, PartialEq, Eq, FromPrimitive)] #[repr(u8)] pub enum OutputType { /// An standard non-coinbase output. @@ -52,16 +54,18 @@ pub enum OutputType { ContractConstitutionProposal = 6, /// Output that indicates acceptance of an existing contract constitution amendment proposal. ContractConstitutionChangeAcceptance = 7, + /// Output that defines and amendment of a contract constitution amendment + ContractAmendment = 8, // TODO: Remove these deprecated flags - NonFungible = 8, - AssetRegistration = 9, - MintNonFungible = 10, - BurnNonFungible = 11, - SidechainInitialCheckpoint = 12, - SidechainCheckpoint = 13, - CommitteeInitialDefinition = 14, - CommitteeDefinition = 15, + NonFungible = 9, + AssetRegistration = 10, + MintNonFungible = 11, + BurnNonFungible = 12, + SidechainInitialCheckpoint = 13, + SidechainCheckpoint = 14, + CommitteeInitialDefinition = 15, + CommitteeDefinition = 16, } impl OutputType { @@ -72,16 +76,8 @@ impl OutputType { /// Returns the OutputType that corresponds to this OutputType. If the byte does not correspond to any OutputType, /// None is returned. - pub fn from_byte(bit: u8) -> Option { - if !Self::is_valid_byte(bit) { - return None; - } - // SAFETY: bit has been checked for validity before transmute is called - Some(unsafe { std::mem::transmute(bit) }) - } - - fn is_valid_byte(bit: u8) -> bool { - bit <= 15 + pub fn from_byte(value: u8) -> Option { + FromPrimitive::from_u8(value) } } @@ -133,7 +129,8 @@ mod tests { fn it_converts_from_byte_to_output_type() { assert_eq!(OutputType::from_byte(0), Some(OutputType::Standard)); assert_eq!(OutputType::from_byte(1), Some(OutputType::Coinbase)); - assert_eq!(OutputType::from_byte(15), Some(OutputType::CommitteeDefinition)); - assert_eq!(OutputType::from_byte(16), None); + assert_eq!(OutputType::from_byte(15), Some(OutputType::CommitteeInitialDefinition)); + assert_eq!(OutputType::from_byte(16), Some(OutputType::CommitteeDefinition)); + assert_eq!(OutputType::from_byte(17), None); } } diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs index 4db021dd6c..e1d1f1e37e 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/sidechain_features.rs @@ -135,6 +135,11 @@ impl SideChainFeaturesBuilder { self } + pub fn with_contract_amendment(mut self, contract_amendment: ContractAmendment) -> Self { + self.features.amendment = Some(contract_amendment); + self + } + pub fn finish(self) -> SideChainFeatures { self.features } diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index 1e23271de2..264ad7792d 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -26,6 +26,7 @@ use tari_common_types::{ types::{Commitment, FixedHash, PublicKey, Signature, ASSET_CHECKPOINT_ID}, }; use tari_core::transactions::transaction_components::{ + ContractAmendment, ContractDefinition, ContractUpdateProposal, OutputFeatures, @@ -359,6 +360,24 @@ impl AssetManager { Ok((tx_id, transaction)) } + + pub async fn create_contract_amendment( + &mut self, + contract_id: FixedHash, + amendment: ContractAmendment, + ) -> Result<(TxId, Transaction), WalletError> { + let output = self + .output_manager + .create_output_with_features(0.into(), OutputFeatures::for_contract_amendment(contract_id, amendment)) + .await?; + + let (tx_id, transaction) = self + .output_manager + .create_send_to_self_with_output(vec![output], ASSET_FPG.into(), None, None) + .await?; + + Ok((tx_id, transaction)) + } } fn convert_to_asset(unblinded_output: DbUnblindedOutput) -> Result { diff --git a/base_layer/wallet/src/assets/asset_manager_handle.rs b/base_layer/wallet/src/assets/asset_manager_handle.rs index 91852d0d7b..4f7f3a62ed 100644 --- a/base_layer/wallet/src/assets/asset_manager_handle.rs +++ b/base_layer/wallet/src/assets/asset_manager_handle.rs @@ -25,6 +25,7 @@ use tari_common_types::{ types::{Commitment, FixedHash, PublicKey, Signature}, }; use tari_core::transactions::transaction_components::{ + ContractAmendment, ContractDefinition, ContractUpdateProposal, OutputFeatures, @@ -284,4 +285,25 @@ impl AssetManagerHandle { }), } } + + pub async fn create_contract_amendment( + &mut self, + contract_id: &FixedHash, + amendment: &ContractAmendment, + ) -> Result<(TxId, Transaction), WalletError> { + match self + .handle + .call(AssetManagerRequest::CreateContractAmendment { + contract_id: *contract_id, + contract_amendment: Box::new(amendment.clone()), + }) + .await?? + { + AssetManagerResponse::CreateContractAmendment { transaction, tx_id } => Ok((tx_id, *transaction)), + _ => Err(WalletError::UnexpectedApiResponse { + method: "create_contract_amendment".to_string(), + api: "AssetManagerService".to_string(), + }), + } + } } diff --git a/base_layer/wallet/src/assets/contract_amendment_file_format.rs b/base_layer/wallet/src/assets/contract_amendment_file_format.rs new file mode 100644 index 0000000000..65e4ac720b --- /dev/null +++ b/base_layer/wallet/src/assets/contract_amendment_file_format.rs @@ -0,0 +1,78 @@ +// 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::convert::{TryFrom, TryInto}; + +use serde::{Deserialize, Serialize}; +use tari_common_types::types::{PrivateKey, PublicKey, Signature}; +use tari_core::transactions::transaction_components::{CommitteeSignatures, ContractAmendment}; +use tari_utilities::hex::Hex; + +use super::ConstitutionDefinitionFileFormat; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContractAmendmentFileFormat { + pub proposal_id: u64, + pub validator_committee: Vec, + pub validator_signatures: Vec, + pub updated_constitution: ConstitutionDefinitionFileFormat, + pub activation_window: u64, +} + +impl TryFrom for ContractAmendment { + type Error = String; + + fn try_from(value: ContractAmendmentFileFormat) -> Result { + let validator_signature_vec: Vec = value + .validator_signatures + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?; + let validator_signatures = + CommitteeSignatures::try_from(validator_signature_vec).map_err(|e| format!("{}", e))?; + + Ok(Self { + proposal_id: value.proposal_id, + validator_committee: value.validator_committee.try_into().map_err(|e| format!("{}", e))?, + validator_signatures, + updated_constitution: value.updated_constitution.try_into()?, + activation_window: value.activation_window, + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SignatureFileFormat { + pub public_nonce: String, + pub signature: String, +} + +impl TryFrom for Signature { + type Error = String; + + fn try_from(value: SignatureFileFormat) -> Result { + let public_key = PublicKey::from_hex(&value.public_nonce).map_err(|e| format!("{}", e))?; + let signature = PrivateKey::from_hex(&value.signature).map_err(|e| format!("{}", e))?; + + Ok(Signature::new(public_key, signature)) + } +} diff --git a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs index d667fa2d09..533c4294d0 100644 --- a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs +++ b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs @@ -226,6 +226,19 @@ impl AssetManagerService { tx_id, }) }, + AssetManagerRequest::CreateContractAmendment { + contract_id, + contract_amendment, + } => { + let (tx_id, transaction) = self + .manager + .create_contract_amendment(contract_id, *contract_amendment) + .await?; + Ok(AssetManagerResponse::CreateContractAmendment { + transaction: Box::new(transaction), + tx_id, + }) + }, } } } diff --git a/base_layer/wallet/src/assets/infrastructure/mod.rs b/base_layer/wallet/src/assets/infrastructure/mod.rs index 98e36159e0..e4506f6ea2 100644 --- a/base_layer/wallet/src/assets/infrastructure/mod.rs +++ b/base_layer/wallet/src/assets/infrastructure/mod.rs @@ -27,6 +27,7 @@ use tari_common_types::{ types::{Commitment, FixedHash, PublicKey, Signature}, }; use tari_core::transactions::transaction_components::{ + ContractAmendment, ContractDefinition, ContractUpdateProposal, OutputFeatures, @@ -90,6 +91,10 @@ pub enum AssetManagerRequest { contract_id: FixedHash, update_proposal: Box, }, + CreateContractAmendment { + contract_id: FixedHash, + contract_amendment: Box, + }, } pub enum AssetManagerResponse { @@ -104,4 +109,5 @@ pub enum AssetManagerResponse { CreateContractAcceptance { transaction: Box, tx_id: TxId }, CreateContractUpdateProposalAcceptance { transaction: Box, tx_id: TxId }, CreateContractUpdateProposal { transaction: Box, tx_id: TxId }, + CreateContractAmendment { transaction: Box, tx_id: TxId }, } diff --git a/base_layer/wallet/src/assets/mod.rs b/base_layer/wallet/src/assets/mod.rs index 5e30e27612..138138f962 100644 --- a/base_layer/wallet/src/assets/mod.rs +++ b/base_layer/wallet/src/assets/mod.rs @@ -32,9 +32,11 @@ pub use asset_manager_handle::AssetManagerHandle; pub(crate) mod infrastructure; mod constitution_definition_file_format; +mod contract_amendment_file_format; mod contract_definition_file_format; 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; diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index 087a20ac2e..842246aa96 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -195,4 +195,19 @@ Feature: Wallet CLI And I publish a contract update proposal from file "fixtures/contract_update_proposal.json" on wallet WALLET via command line And mining node MINE mines 8 blocks Then wallet WALLET has at least 3 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled - Then WALLET is connected to BASE \ No newline at end of file + Then WALLET is connected to BASE + + @dan @critical + Scenario: As a user I want to publish a contract amendment via command line + Given I have a base node BASE + And I have wallet WALLET connected to base node BASE + And I have mining node MINE connected to base node BASE and wallet WALLET + And mining node MINE mines 4 blocks + Then I wait for wallet WALLET to have at least 1000000 uT + And I publish a contract definition from file "fixtures/contract_definition.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + And I publish a contract amendment from file "fixtures/contract_amendment.json" on wallet WALLET via command line + And mining node MINE mines 8 blocks + Then wallet WALLET has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled + Then WALLET is connected to BASE diff --git a/integration_tests/features/support/wallet_cli_steps.js b/integration_tests/features/support/wallet_cli_steps.js index 7233960913..3bcd0db3aa 100644 --- a/integration_tests/features/support/wallet_cli_steps.js +++ b/integration_tests/features/support/wallet_cli_steps.js @@ -343,3 +343,18 @@ Then( console.log(output.buffer); } ); + +Then( + "I publish a contract amendment from file {string} on wallet {word} via command line", + { timeout: 120 * 1000 }, + async function (relative_file_path, wallet_name) { + let absolute_path = path.resolve(relative_file_path); + let wallet = this.getWallet(wallet_name); + + let output = await wallet_run_command( + wallet, + `contract publish-amendment ${absolute_path}` + ); + console.log(output.buffer); + } +); diff --git a/integration_tests/fixtures/contract_amendment.json b/integration_tests/fixtures/contract_amendment.json new file mode 100644 index 0000000000..13b31ce6f1 --- /dev/null +++ b/integration_tests/fixtures/contract_amendment.json @@ -0,0 +1,44 @@ +{ + "proposal_id": 1, + "validator_committee": [ + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" + ], + "validator_signatures": [ + { + "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", + "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" + }, + { + "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", + "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" + }, + { + "public_nonce": "3431860a4f70ddd6748d759cf66179321809e1c120a97cbdbbf2c01af5c8802f", + "signature": "be1b1e7cd18210bfced717d39bebc2534b31274976fb141856d9ee2bfe571900" + } + ], + "updated_constitution": { + "contract_id": "90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177", + "validator_committee": [ + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44", + "ccac168b8edd67b10d152d1ed2337efc65da9fc0b6256dd49b3c559032553d44" + ], + "acceptance_parameters": { + "acceptance_period_expiry": 50, + "minimum_quorum_required": 2 + }, + "consensus": "MerkleRoot", + "checkpoint_parameters": { + "minimum_quorum_required": 2, + "abandoned_interval": 100 + }, + "constitution_change_rules": { + "change_flags": 1 + }, + "initial_reward": 5 + }, + "activation_window": 100 +}