Skip to content

Commit

Permalink
feat(wallet): new command to publish a contract update proposal (#4188)
Browse files Browse the repository at this point in the history
Description
---
New subcommand in the wallet CLI (`contract publish-update-proposal`):
* Reads a proposal description from a JSON file
* Creates a transaction with the appropriate features
* Publishes the transaction into the network

Motivation and Context
---
We need a way to publish a contract update proposal into the blockchain, via a wallet command

How Has This Been Tested?
---
The new integration test pass
  • Loading branch information
mrnaveira authored Jun 15, 2022
1 parent 4498800 commit 0e3bee0
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 7 deletions.
43 changes: 42 additions & 1 deletion applications/tari_console_wallet/src/automation/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ use futures::FutureExt;
use log::*;
use sha2::Sha256;
use strum_macros::{Display, EnumIter, EnumString};
use tari_common_types::{emoji::EmojiId, transaction::TxId, types::PublicKey};
use tari_common_types::{
emoji::EmojiId,
transaction::TxId,
types::{FixedHash, PublicKey},
};
use tari_comms::{
connectivity::{ConnectivityEvent, ConnectivityRequester},
multiaddr::Multiaddr,
Expand All @@ -47,6 +51,7 @@ use tari_core::transactions::{
CheckpointParameters,
ContractAcceptanceRequirements,
ContractDefinition,
ContractUpdateProposal,
SideChainConsensus,
SideChainFeatures,
TransactionOutput,
Expand All @@ -61,6 +66,7 @@ use tari_wallet::{
ConstitutionDefinitionFileFormat,
ContractDefinitionFileFormat,
ContractSpecificationFileFormat,
ContractUpdateProposalFileFormat,
},
error::WalletError,
output_manager_service::handle::OutputManagerHandle,
Expand Down Expand Up @@ -791,6 +797,7 @@ async fn handle_contract_definition_command(
ContractSubcommand::InitConstitution(args) => init_contract_constitution_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,
}
}

Expand Down Expand Up @@ -951,6 +958,40 @@ async fn publish_contract_constitution(wallet: &WalletSqlite, args: PublishFileA
Ok(())
}

async fn publish_contract_update_proposal(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 update_proposal: ContractUpdateProposalFileFormat =
serde_json::from_reader(file_reader).map_err(|e| CommandError::JsonFile(e.to_string()))?;
let contract_id_hex = update_proposal.updated_constitution.contract_id.clone();
let contract_id = FixedHash::from_hex(&contract_id_hex).map_err(|e| CommandError::JsonFile(e.to_string()))?;
let update_proposal_features = ContractUpdateProposal::try_from(update_proposal).map_err(CommandError::JsonFile)?;

let mut asset_manager = wallet.asset_manager.clone();
let (tx_id, transaction) = asset_manager
.create_update_proposal(&contract_id, &update_proposal_features)
.await?;

let message = format!(
"Contract update proposal {} for contract {}",
update_proposal_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 update proposal transaction submitted with tx_id={} for contract with contract_id={}",
tx_id, contract_id_hex
);

Ok(())
}

fn write_utxos_to_csv_file(utxos: Vec<UnblindedOutput>, file_path: PathBuf) -> Result<(), CommandError> {
let factory = PedersenCommitmentFactory::default();
let file = File::create(file_path).map_err(|e| CommandError::CSVFile(e.to_string()))?;
Expand Down
8 changes: 8 additions & 0 deletions applications/tari_console_wallet/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ pub enum ContractSubcommand {

/// Creates and publishes a contract definition UTXO from the JSON spec file.
PublishConstitution(PublishFileArgs),

/// Creates and publishes a contract update proposal UTXO from the JSON spec file.
PublishUpdateProposal(PublishFileArgs),
}

#[derive(Debug, Args, Clone)]
Expand Down Expand Up @@ -256,3 +259,8 @@ pub struct InitConstitutionArgs {
pub struct PublishFileArgs {
pub file_path: PathBuf,
}

#[derive(Debug, Args, Clone)]
pub struct PublishUpdateProposalArgs {
pub file_path: PathBuf,
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ use tari_common_types::types::{Commitment, FixedHash, PublicKey, Signature};
use tari_crypto::ristretto::pedersen::PedersenCommitment;
use tari_utilities::ByteArray;

use super::{ContractAcceptance, ContractDefinition, OutputFeaturesVersion, SideChainFeaturesBuilder};
use super::{
ContractAcceptance,
ContractDefinition,
ContractUpdateProposal,
OutputFeaturesVersion,
SideChainFeaturesBuilder,
};
use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes},
transactions::{
Expand Down Expand Up @@ -311,6 +317,21 @@ impl OutputFeatures {
}
}

pub fn for_contract_update_proposal(
contract_id: FixedHash,
update_proposal: ContractUpdateProposal,
) -> OutputFeatures {
Self {
output_type: OutputType::ContractConstitutionProposal,
sidechain_features: Some(
SideChainFeaturesBuilder::new(contract_id)
.with_update_proposal(update_proposal)
.finish(),
),
..Default::default()
}
}

pub fn unique_asset_id(&self) -> Option<&[u8]> {
self.unique_id.as_deref()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ impl SideChainFeaturesBuilder {
self
}

pub fn with_update_proposal(mut self, update_proposal: ContractUpdateProposal) -> Self {
self.features.update_proposal = Some(update_proposal);
self
}

pub fn finish(self) -> SideChainFeatures {
self.features
}
Expand Down
22 changes: 22 additions & 0 deletions base_layer/wallet/src/assets/asset_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use tari_common_types::{
};
use tari_core::transactions::transaction_components::{
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
OutputType,
SideChainFeatures,
Expand Down Expand Up @@ -309,6 +310,27 @@ impl<T: OutputManagerBackend + 'static> AssetManager<T> {

Ok((tx_id, transaction))
}

pub async fn create_update_proposal(
&mut self,
contract_id: FixedHash,
update_proposal: ContractUpdateProposal,
) -> Result<(TxId, Transaction), WalletError> {
let output = self
.output_manager
.create_output_with_features(
0.into(),
OutputFeatures::for_contract_update_proposal(contract_id, update_proposal),
)
.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<Asset, WalletError> {
Expand Down
22 changes: 22 additions & 0 deletions base_layer/wallet/src/assets/asset_manager_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use tari_common_types::{
};
use tari_core::transactions::transaction_components::{
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
SideChainFeatures,
TemplateParameter,
Expand Down Expand Up @@ -235,4 +236,25 @@ impl AssetManagerHandle {
}),
}
}

pub async fn create_update_proposal(
&mut self,
contract_id: &FixedHash,
update_proposal: &ContractUpdateProposal,
) -> Result<(TxId, Transaction), WalletError> {
match self
.handle
.call(AssetManagerRequest::CreateContractUpdateProposal {
contract_id: *contract_id,
update_proposal: Box::new(update_proposal.clone()),
})
.await??
{
AssetManagerResponse::CreateContractUpdateProposal { transaction, tx_id } => Ok((tx_id, *transaction)),
_ => Err(WalletError::UnexpectedApiResponse {
method: "create_update_proposal".to_string(),
api: "AssetManagerService".to_string(),
}),
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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::ContractUpdateProposal;
use tari_utilities::hex::Hex;

use super::ConstitutionDefinitionFileFormat;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ContractUpdateProposalFileFormat {
pub proposal_id: u64,
pub signature: SignatureFileFormat,
pub updated_constitution: ConstitutionDefinitionFileFormat,
}

impl TryFrom<ContractUpdateProposalFileFormat> for ContractUpdateProposal {
type Error = String;

fn try_from(value: ContractUpdateProposalFileFormat) -> Result<Self, Self::Error> {
Ok(Self {
proposal_id: value.proposal_id,
signature: value.signature.try_into()?,
updated_constitution: value.updated_constitution.try_into()?,
})
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignatureFileFormat {
pub public_nonce: String,
pub signature: String,
}

impl TryFrom<SignatureFileFormat> for Signature {
type Error = String;

fn try_from(value: SignatureFileFormat) -> Result<Self, Self::Error> {
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ impl<T: OutputManagerBackend + 'static> AssetManagerService<T> {
tx_id,
})
},
AssetManagerRequest::CreateContractUpdateProposal {
contract_id,
update_proposal,
} => {
let (tx_id, transaction) = self
.manager
.create_update_proposal(contract_id, *update_proposal)
.await?;
Ok(AssetManagerResponse::CreateContractUpdateProposal {
transaction: Box::new(transaction),
tx_id,
})
},
}
}
}
6 changes: 6 additions & 0 deletions base_layer/wallet/src/assets/infrastructure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use tari_common_types::{
};
use tari_core::transactions::transaction_components::{
ContractDefinition,
ContractUpdateProposal,
OutputFeatures,
SideChainFeatures,
TemplateParameter,
Expand Down Expand Up @@ -79,6 +80,10 @@ pub enum AssetManagerRequest {
validator_node_public_key: Box<PublicKey>,
signature: Box<Signature>,
},
CreateContractUpdateProposal {
contract_id: FixedHash,
update_proposal: Box<ContractUpdateProposal>,
},
}

pub enum AssetManagerResponse {
Expand All @@ -91,4 +96,5 @@ pub enum AssetManagerResponse {
CreateConstitutionDefinition { transaction: Box<Transaction>, tx_id: TxId },
CreateContractDefinition { transaction: Box<Transaction>, tx_id: TxId },
CreateContractAcceptance { transaction: Box<Transaction>, tx_id: TxId },
CreateContractUpdateProposal { transaction: Box<Transaction>, tx_id: TxId },
}
2 changes: 2 additions & 0 deletions base_layer/wallet/src/assets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub(crate) mod infrastructure;

mod constitution_definition_file_format;
mod contract_definition_file_format;
mod contract_update_proposal_file_format;

pub use constitution_definition_file_format::{ConstitutionChangeRulesFileFormat, ConstitutionDefinitionFileFormat};
pub use contract_definition_file_format::{ContractDefinitionFileFormat, ContractSpecificationFileFormat};
pub use contract_update_proposal_file_format::ContractUpdateProposalFileFormat;
7 changes: 5 additions & 2 deletions integration_tests/features/ValidatorNode.feature
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ Feature: Validator Node
And I have wallet WALLET1 connected to all seed nodes
When I mine 9 blocks using wallet WALLET1 on NODE1
Then I wait for wallet WALLET1 to have at least 1000000 uT
And I publish a contract definition from file "fixtures/contract_definition.json" on wallet WALLET1 via command line
When I mine 8 blocks using wallet WALLET1 on NODE1
Then wallet WALLET1 has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled
And I have a validator node VN1 connected to base node NODE1 and wallet WALLET1 with "constitiution_auto_accept" set to "false"
Then I publish a contract acceptance transaction for the validator node VN1
When I mine 4 blocks using wallet WALLET1 on NODE1
Then wallet WALLET1 has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled
When I mine 8 blocks using wallet WALLET1 on NODE1
Then wallet WALLET1 has at least 2 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled

@dan @broken
Scenario: Contract auto acceptance
Expand Down
18 changes: 18 additions & 0 deletions integration_tests/features/WalletCli.feature
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,22 @@ Feature: Wallet CLI
And I publish a contract constitution from file "fixtures/contract_constitution.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

@dan @critical
Scenario: As a user I want to publish a contract update proposal 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 constitution from file "fixtures/contract_constitution.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
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
2 changes: 1 addition & 1 deletion integration_tests/features/support/validator_node_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Then(
let dan_node = this.getNode(vn_name);
let grpc_dan_node = await dan_node.createGrpcClient();
let response = await grpc_dan_node.publishContractAcceptance(
"f665775dbbf4e428e5c8c2bb1c5e7d2e508e93c83250c495ac617a0a1fb2d76d" // contract_id
"90b1da4524ea0e9479040d906db9194d8af90f28d05ff2d64c0a82eb93125177" // contract_id
);
expect(response.status).to.be.equal("Accepted");
console.log({ response });
Expand Down
Loading

0 comments on commit 0e3bee0

Please sign in to comment.