From b4991a471cb3a6db2a54b623c0afc09f71ae3dc4 Mon Sep 17 00:00:00 2001 From: mrnaveira <47919901+mrnaveira@users.noreply.github.com> Date: Thu, 26 May 2022 17:11:47 +0100 Subject: [PATCH] feat(wallet): new command to publish a contract definition transaction (#4133) Description --- * Created a new transaction output features for the Contract Definition * For simplicity, I decided to leave out fields that could be more coupled to the template format. * The `contract_id` is calculated as a hash of the contents of the contract definition, in the line of what is described in the RFCs. I used `ConsensusHashWriter` to make it totally consistent. * I created a new Output Features version to signal the consensus breaking changes. * Had to update the genesis block struct to include the new features * Had to update the gRPC types to include the new features * Created a new command in the tari console wallet (`publish-contract-definition`): 1. Reads the contract definition from a JSON file. I defined auxiliary structs (`base_layer/wallet/src/assets/contract_definition.rs`) to decouple the file format from the output features. 2. Created a new transaction with the `CONTRACT_DEFNITION` flag and the new Contract Definition output features. 3. Publishes the corresponding UTXO into the network. * Right now the base layer does not perform any check in the values in the contract definition. In the future we want to implement custom consensus rules for contract definition (like avoiding duplicates, spending requirements, etc.) but I purposely left that out of this PR for simplicity. Motivation and Context --- As a user, I want to be able to publish a new contract definition into the network through the console wallet. How Has This Been Tested? --- * Created a new unit test for the consensus encoding of the new Contract Definition output features * Created a new integration test for the wallet CLI that reads the contract definition from a JSON file and successfully publishes the transaction --- Cargo.lock | 2 + applications/tari_app_grpc/proto/types.proto | 24 +- .../src/conversions/sidechain_features.rs | 132 ++++++++- applications/tari_console_wallet/Cargo.toml | 2 + .../src/automation/command_parser.rs | 22 ++ .../src/automation/commands.rs | 40 ++- .../src/automation/error.rs | 2 + base_layer/core/src/blocks/genesis_block.rs | 2 +- base_layer/core/src/proto/transaction.proto | 24 ++ base_layer/core/src/proto/transaction.rs | 130 +++++++++ .../transaction_components/output_features.rs | 57 +++- .../output_features_version.rs | 2 + .../side_chain/contract_definition.rs | 275 ++++++++++++++++++ .../transaction_components/side_chain/mod.rs | 11 +- .../side_chain/sidechain_features.rs | 38 +++ base_layer/wallet/src/assets/asset_manager.rs | 25 +- .../wallet/src/assets/asset_manager_handle.rs | 26 +- .../assets/contract_definition_file_format.rs | 94 ++++++ .../infrastructure/asset_manager_service.rs | 8 + .../wallet/src/assets/infrastructure/mod.rs | 11 +- base_layer/wallet/src/assets/mod.rs | 3 + integration_tests/features/WalletCli.feature | 12 + .../features/support/wallet_cli_steps.js | 16 + .../fixtures/contract_definition.json | 23 ++ 24 files changed, 962 insertions(+), 19 deletions(-) create mode 100644 base_layer/core/src/transactions/transaction_components/side_chain/contract_definition.rs create mode 100644 base_layer/wallet/src/assets/contract_definition_file_format.rs create mode 100644 integration_tests/fixtures/contract_definition.json diff --git a/Cargo.lock b/Cargo.lock index d254c2c703..75d1291758 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6823,6 +6823,8 @@ dependencies = [ "regex", "rpassword", "rustyline", + "serde", + "serde_json", "sha2 0.9.9", "strum 0.22.0", "strum_macros 0.22.0", diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index 1a0be8d0df..45dd38a4da 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -230,7 +230,7 @@ message OutputFeatures { message SideChainFeatures { bytes contract_id = 1; - // ContractDefinition definition = 2; + ContractDefinition definition = 2; ContractConstitution constitution = 3; } @@ -306,6 +306,28 @@ message CommitteeDefinitionFeatures { uint64 effective_sidechain_height = 2; } +message ContractDefinition { + bytes contract_id = 1; + bytes contract_name = 2; + bytes contract_issuer = 3; + ContractSpecification contract_spec = 4; +} + +message ContractSpecification { + bytes runtime = 1; + repeated PublicFunction public_functions = 2; +} + +message PublicFunction { + bytes name = 1; + FunctionRef function = 2; +} + +message FunctionRef { + bytes template_id = 1; + uint32 function_id = 2; +} + // The components of the block or transaction. The same struct can be used for either, since in Mimblewimble, // cut-through means that blocks and transactions have the same structure. The inputs, outputs and kernels should // be sorted by their Blake2b-256bit digest hash diff --git a/applications/tari_app_grpc/src/conversions/sidechain_features.rs b/applications/tari_app_grpc/src/conversions/sidechain_features.rs index 9c2c7a491b..2e2e6a2323 100644 --- a/applications/tari_app_grpc/src/conversions/sidechain_features.rs +++ b/applications/tari_app_grpc/src/conversions/sidechain_features.rs @@ -22,14 +22,19 @@ use std::convert::{TryFrom, TryInto}; -use tari_common_types::types::PublicKey; +use tari_common_types::types::{FixedHash, PublicKey}; use tari_core::transactions::transaction_components::{ + vec_into_fixed_string, CheckpointParameters, CommitteeMembers, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptanceRequirements, ContractConstitution, + ContractDefinition, + ContractSpecification, + FunctionRef, + PublicFunction, RequirementsForConstitutionChange, SideChainConsensus, SideChainFeatures, @@ -42,6 +47,7 @@ impl From for grpc::SideChainFeatures { fn from(value: SideChainFeatures) -> Self { Self { contract_id: value.contract_id.to_vec(), + definition: value.definition.map(Into::into), constitution: value.constitution.map(Into::into), } } @@ -51,15 +57,139 @@ impl TryFrom for SideChainFeatures { type Error = String; fn try_from(features: grpc::SideChainFeatures) -> Result { + let definition = features.definition.map(ContractDefinition::try_from).transpose()?; let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; Ok(Self { contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, + definition, constitution, }) } } +//---------------------------------- ContractDefinition --------------------------------------------// + +impl TryFrom for ContractDefinition { + type Error = String; + + fn try_from(value: grpc::ContractDefinition) -> Result { + let contract_id = FixedHash::try_from(value.contract_id).map_err(|err| format!("{:?}", err))?; + + let contract_name = vec_into_fixed_string(value.contract_name); + + let contract_issuer = + PublicKey::from_bytes(value.contract_issuer.as_bytes()).map_err(|err| format!("{:?}", err))?; + + let contract_spec = value + .contract_spec + .map(ContractSpecification::try_from) + .ok_or_else(|| "contract_spec is missing".to_string())? + .map_err(|err| err)?; + + Ok(Self { + contract_id, + contract_name, + contract_issuer, + contract_spec, + }) + } +} + +impl From for grpc::ContractDefinition { + fn from(value: ContractDefinition) -> Self { + let contract_id = value.contract_id.as_bytes().to_vec(); + let contract_name = value.contract_name.as_bytes().to_vec(); + let contract_issuer = value.contract_issuer.as_bytes().to_vec(); + + Self { + contract_id, + contract_name, + contract_issuer, + contract_spec: Some(value.contract_spec.into()), + } + } +} + +impl TryFrom for ContractSpecification { + type Error = String; + + fn try_from(value: grpc::ContractSpecification) -> Result { + let runtime = vec_into_fixed_string(value.runtime); + let public_functions = value + .public_functions + .into_iter() + .map(PublicFunction::try_from) + .collect::>()?; + + Ok(Self { + runtime, + public_functions, + }) + } +} + +impl From for grpc::ContractSpecification { + fn from(value: ContractSpecification) -> Self { + let public_functions = value.public_functions.into_iter().map(|f| f.into()).collect(); + Self { + runtime: value.runtime.as_bytes().to_vec(), + public_functions, + } + } +} + +impl TryFrom for PublicFunction { + type Error = String; + + fn try_from(value: grpc::PublicFunction) -> Result { + let function = value + .function + .map(FunctionRef::try_from) + .ok_or_else(|| "function is missing".to_string())? + .map_err(|err| err)?; + + Ok(Self { + name: vec_into_fixed_string(value.name), + function, + }) + } +} + +impl From for grpc::PublicFunction { + fn from(value: PublicFunction) -> Self { + Self { + name: value.name.as_bytes().to_vec(), + function: Some(value.function.into()), + } + } +} + +impl TryFrom for FunctionRef { + type Error = String; + + fn try_from(value: grpc::FunctionRef) -> Result { + let template_id = FixedHash::try_from(value.template_id).map_err(|err| format!("{:?}", err))?; + let function_id = u16::try_from(value.function_id).map_err(|_| "Invalid function_id: overflowed u16")?; + + Ok(Self { + template_id, + function_id, + }) + } +} + +impl From for grpc::FunctionRef { + fn from(value: FunctionRef) -> Self { + let template_id = value.template_id.as_bytes().to_vec(); + + Self { + template_id, + function_id: value.function_id.into(), + } + } +} + //---------------------------------- ContractConstitution --------------------------------------------// impl From for grpc::ContractConstitution { fn from(value: ContractConstitution) -> Self { diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index 400e737a18..2116cde3df 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -42,6 +42,8 @@ qrcode = { version = "0.12" } regex = "1.5.4" rpassword = "5.0" rustyline = "9.0" +serde = "1.0.136" +serde_json = "1.0.79" strum = "0.22" strum_macros = "0.22" thiserror = "1.0.26" diff --git a/applications/tari_console_wallet/src/automation/command_parser.rs b/applications/tari_console_wallet/src/automation/command_parser.rs index 48c50ce91e..201449f3bd 100644 --- a/applications/tari_console_wallet/src/automation/command_parser.rs +++ b/applications/tari_console_wallet/src/automation/command_parser.rs @@ -68,6 +68,7 @@ impl Display for ParsedCommand { CreateInitialCheckpoint => "create-initial-checkpoint", CreateCommitteeDefinition => "create-committee-definition", RevalidateWalletDb => "revalidate-wallet-db", + PublishContractDefinition => "publish-contract-definition", }; let args = self @@ -94,6 +95,7 @@ pub enum ParsedArgument { Address(Multiaddr), Negotiated(bool), Hash(Vec), + JSONFileName(String), } impl Display for ParsedArgument { @@ -112,6 +114,7 @@ impl Display for ParsedArgument { Address(v) => write!(f, "{}", v), Negotiated(v) => write!(f, "{}", v), Hash(v) => write!(f, "{}", v.to_hex()), + JSONFileName(v) => write!(f, "{}", v), } } } @@ -148,6 +151,7 @@ pub fn parse_command(command: &str) -> Result { CreateInitialCheckpoint => parser_builder(args).pub_key().text().build()?, CreateCommitteeDefinition => parser_builder(args).pub_key().pub_key_array().build()?, RevalidateWalletDb => Vec::new(), + PublishContractDefinition => parse_publish_contract_definition(args)?, }; Ok(ParsedCommand { command, args }) @@ -483,6 +487,24 @@ fn parse_coin_split(mut args: SplitWhitespace) -> Result, Pa Ok(parsed_args) } +fn parse_publish_contract_definition(mut args: SplitWhitespace) -> Result, ParseError> { + let mut parsed_args = Vec::new(); + + let usage = "Usage:\n publish-contract-definition\n publish-contract-definition --json-file "; + + let arg = args.next().ok_or_else(|| ParseError::Empty("json-file".to_string()))?; + if arg != "--json-file" { + return Err(ParseError::Empty(format!("'--json-file' qualifier\n {}", usage))); + } + + let file_name = args + .next() + .ok_or_else(|| ParseError::Empty(format!("file name\n {}", usage)))?; + parsed_args.push(ParsedArgument::JSONFileName(file_name.to_string())); + + Ok(parsed_args) +} + #[cfg(test)] mod test { use std::str::FromStr; diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index ed29ae8d44..9eed03ba2e 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::{LineWriter, Write}, + io::{BufReader, LineWriter, Write}, time::{Duration, Instant}, }; @@ -45,12 +45,12 @@ use tari_comms::{ use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; use tari_core::transactions::{ tari_amount::{uT, MicroTari, Tari}, - transaction_components::{TransactionOutput, UnblindedOutput}, + transaction_components::{ContractDefinition, TransactionOutput, UnblindedOutput}, }; use tari_crypto::{keys::PublicKey as PublicKeyTrait, ristretto::pedersen::PedersenCommitmentFactory}; use tari_utilities::{hex::Hex, ByteArray, Hashable}; use tari_wallet::{ - assets::KEY_MANAGER_ASSET_BRANCH, + assets::{ContractDefinitionFileFormat, KEY_MANAGER_ASSET_BRANCH}, error::WalletError, key_manager_service::KeyManagerInterface, output_manager_service::handle::OutputManagerHandle, @@ -97,6 +97,7 @@ pub enum WalletCommand { CreateInitialCheckpoint, CreateCommitteeDefinition, RevalidateWalletDb, + PublishContractDefinition, } #[derive(Debug)] @@ -908,6 +909,39 @@ pub async fn command_runner( .await .map_err(CommandError::TransactionServiceError)?; }, + PublishContractDefinition => { + // open the JSON file with the contract definition values + let file_path = match parsed.args.get(0) { + Some(ParsedArgument::JSONFileName(ref file_path)) => Ok(file_path), + _ => Err(CommandError::Argument), + }?; + let file = File::open(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.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!"); + }, } } diff --git a/applications/tari_console_wallet/src/automation/error.rs b/applications/tari_console_wallet/src/automation/error.rs index 370e4f35fc..2bb1d2f533 100644 --- a/applications/tari_console_wallet/src/automation/error.rs +++ b/applications/tari_console_wallet/src/automation/error.rs @@ -68,6 +68,8 @@ pub enum CommandError { HexError(#[from] HexError), #[error("Error `{0}`")] ShaError(String), + #[error("JSON file error `{0}`")] + JSONFile(String), } impl From for ExitError { diff --git a/base_layer/core/src/blocks/genesis_block.rs b/base_layer/core/src/blocks/genesis_block.rs index 81a32776ac..59a5bb726f 100644 --- a/base_layer/core/src/blocks/genesis_block.rs +++ b/base_layer/core/src/blocks/genesis_block.rs @@ -248,7 +248,7 @@ fn get_dibbler_genesis_block_raw() -> Block { asset: None, mint_non_fungible: None, sidechain_checkpoint: None, - committee_definition: None + committee_definition: None, }, Commitment::from_hex("e2b9ee8fdf05f9fa8fd7598d4568b539eef694e58cdae84c779140271a96d733 ").unwrap(), BulletRangeProof::from_hex("0c02c6d9bdbd1c21b29ee0f83bed597ed07f71a60f99ddcbc02550059c4c08020438f8fc25c69160dc6af81f84c037fc79b9f4a7baa93ab4d6ef1640356d9b575efe1f3f8b40f6c64d1a964aab215491f79738ccac1712d756607626442dda37ac30010dc663985153786cc53c865c01bfec186a803c1edb1a34efa3088ec221016a63cdbd2b58fa4c258bfbf0cff6b793ab62f5db0fb781f046effb5f4e7c0a956c2e042e3f0c16a72f20a624625fa6dc0b742e49e0158a50c8abde54834e04bb35baef0c258da30b738256549e3a2612ff89b4f6bfe82d16aa10b38daabe0df6b922717cb4b1604ab97a2a5efa4d325beb56c5419cff185d61e1a0fc9e374098bf4a10404d788141e2c77de222d68c14b421b62f300898c25487f491aff26be85e54c011e90cc96aff6b31993ce74233674fb150de929fbc259bcc7808a84432cf28bf83c2a0fbf2b47a6244fbafa02ca4f5c9d46c5380fe8eaed734f1d56e769e59800137900cb5905191bbb463cbcb4ea0a2073d716f18878ed4455d19426a2c1133bf703510bf0f1b3b70e9e5ee6fbb70a8e710fd0a4b8f37eacfdeef3df66e461f16ffdb270a7181505b1358f56578840bbfa284444c35160794f0294300ecb3fde701a3f5ed9234e4c196b93fd70633069eeb184ab53685b5324c963a7428094f0c7d4306b5da6ef5fb68d085c32adabe004bebcbf335ee8fc92e5e034edcb035872d08f139e9445539241ff9b9fbebbc0e7b248cbd97fa7c6f3d7823085893c9ced1685d69d2a7cf111f81e086927565c301d4e33639def1139bd5245a0ae9085d5ba10cdc1f89fc7a7fa95cc3aa11784ec40ebf57475ffb4f2b2042018e3dbe905ebd5d0ebe533f36f43f709110372c94258a59e53c9b319adca30c8e9f4f92d5937f994ff36a5bb38a15682187dc8734162f45e169a97a36fb5a05").unwrap(), diff --git a/base_layer/core/src/proto/transaction.proto b/base_layer/core/src/proto/transaction.proto index 5f5b35cb34..d534fff806 100644 --- a/base_layer/core/src/proto/transaction.proto +++ b/base_layer/core/src/proto/transaction.proto @@ -103,6 +103,7 @@ message OutputFeatures { message SideChainFeatures { bytes contract_id = 1; + ContractDefinition definition = 2; ContractConstitution constitution = 3; } @@ -176,6 +177,29 @@ message CommitteeDefinitionFeatures { repeated bytes committee = 1; uint64 effective_sidechain_height = 2; } + +message ContractDefinition { + bytes contract_id = 1; + bytes contract_name = 2; + bytes contract_issuer = 3; + ContractSpecification contract_spec = 4; +} + +message ContractSpecification { + bytes runtime = 1; + repeated PublicFunction public_functions = 2; +} + +message PublicFunction { + bytes name = 1; + FunctionRef function = 2; +} + +message FunctionRef { + bytes template_id = 1; + uint32 function_id = 2; +} + // The components of the block or transaction. The same struct can be used for either, since in Mimblewimble, // cut-through means that blocks and transactions have the same structure. The inputs, outputs and kernels should // be sorted by their Blake2b-256bit digest hash diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index 39e72a3c69..1804613ab1 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -39,6 +39,7 @@ use crate::{ aggregated_body::AggregateBody, tari_amount::MicroTari, transaction_components::{ + vec_into_fixed_string, AssetOutputFeatures, CheckpointParameters, CommitteeDefinitionFeatures, @@ -47,11 +48,15 @@ use crate::{ ConstitutionChangeRules, ContractAcceptanceRequirements, ContractConstitution, + ContractDefinition, + ContractSpecification, + FunctionRef, KernelFeatures, MintNonFungibleFeatures, OutputFeatures, OutputFeaturesVersion, OutputFlags, + PublicFunction, RequirementsForConstitutionChange, SideChainCheckpointFeatures, SideChainConsensus, @@ -343,6 +348,7 @@ impl From for proto::types::SideChainFeatures { fn from(value: SideChainFeatures) -> Self { Self { contract_id: value.contract_id.to_vec(), + definition: value.definition.map(Into::into), constitution: value.constitution.map(Into::into), } } @@ -352,10 +358,12 @@ impl TryFrom for SideChainFeatures { type Error = String; fn try_from(features: proto::types::SideChainFeatures) -> Result { + let definition = features.definition.map(ContractDefinition::try_from).transpose()?; let constitution = features.constitution.map(ContractConstitution::try_from).transpose()?; Ok(Self { contract_id: features.contract_id.try_into().map_err(|_| "Invalid contract_id")?, + definition, constitution, }) } @@ -697,6 +705,128 @@ impl From for proto::types::CommitteeDefinitionFeat } } +//---------------------------------- ContractDefinition --------------------------------------------// + +impl TryFrom for ContractDefinition { + type Error = String; + + fn try_from(value: proto::types::ContractDefinition) -> Result { + let contract_id = FixedHash::try_from(value.contract_id).map_err(|err| format!("{:?}", err))?; + + let contract_name = vec_into_fixed_string(value.contract_name); + + let contract_issuer = + PublicKey::from_bytes(value.contract_issuer.as_bytes()).map_err(|err| format!("{:?}", err))?; + + let contract_spec = value + .contract_spec + .map(ContractSpecification::try_from) + .ok_or_else(|| "contract_spec is missing".to_string())? + .map_err(|err| err)?; + + Ok(Self { + contract_id, + contract_name, + contract_issuer, + contract_spec, + }) + } +} + +impl From for proto::types::ContractDefinition { + fn from(value: ContractDefinition) -> Self { + let contract_id = value.contract_id.as_bytes().to_vec(); + let contract_name = value.contract_name.as_bytes().to_vec(); + let contract_issuer = value.contract_issuer.as_bytes().to_vec(); + + Self { + contract_id, + contract_name, + contract_issuer, + contract_spec: Some(value.contract_spec.into()), + } + } +} + +impl TryFrom for ContractSpecification { + type Error = String; + + fn try_from(value: proto::types::ContractSpecification) -> Result { + let runtime = vec_into_fixed_string(value.runtime); + let public_functions = value + .public_functions + .into_iter() + .map(PublicFunction::try_from) + .collect::>()?; + + Ok(Self { + runtime, + public_functions, + }) + } +} + +impl From for proto::types::ContractSpecification { + fn from(value: ContractSpecification) -> Self { + let public_functions = value.public_functions.into_iter().map(|f| f.into()).collect(); + Self { + runtime: value.runtime.as_bytes().to_vec(), + public_functions, + } + } +} + +impl TryFrom for PublicFunction { + type Error = String; + + fn try_from(value: proto::types::PublicFunction) -> Result { + let function = value + .function + .map(FunctionRef::try_from) + .ok_or_else(|| "function is missing".to_string())? + .map_err(|err| err)?; + + Ok(Self { + name: vec_into_fixed_string(value.name), + function, + }) + } +} + +impl From for proto::types::PublicFunction { + fn from(value: PublicFunction) -> Self { + Self { + name: value.name.as_bytes().to_vec(), + function: Some(value.function.into()), + } + } +} + +impl TryFrom for FunctionRef { + type Error = String; + + fn try_from(value: proto::types::FunctionRef) -> Result { + let template_id = FixedHash::try_from(value.template_id).map_err(|err| format!("{:?}", err))?; + let function_id = u16::try_from(value.function_id).map_err(|_| "Invalid function_id: overflowed u16")?; + + Ok(Self { + template_id, + function_id, + }) + } +} + +impl From for proto::types::FunctionRef { + fn from(value: FunctionRef) -> Self { + let template_id = value.template_id.as_bytes().to_vec(); + + Self { + template_id, + function_id: value.function_id.into(), + } + } +} + //---------------------------------- AggregateBody --------------------------------------------// impl TryFrom for AggregateBody { 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 f8ab4ff8b2..3535f50c3b 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -37,7 +37,7 @@ use tari_common_types::types::{Commitment, FixedHash, PublicKey}; use tari_crypto::ristretto::pedersen::PedersenCommitment; use tari_utilities::ByteArray; -use super::OutputFeaturesVersion; +use super::{ContractDefinition, OutputFeaturesVersion, SideChainFeaturesBuilder}; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}, transactions::{ @@ -282,6 +282,18 @@ impl OutputFeatures { } } + pub fn for_contract_definition(definition: ContractDefinition) -> OutputFeatures { + Self { + flags: OutputFlags::CONTRACT_DEFINITION, + sidechain_features: Some( + SideChainFeaturesBuilder::new(definition.contract_id) + .with_contract_definition(definition) + .finish(), + ), + ..Default::default() + } + } + pub fn unique_asset_id(&self) -> Option<&[u8]> { self.unique_id.as_deref() } @@ -322,7 +334,7 @@ impl ConsensusEncoding for OutputFeatures { self.flags.consensus_encode(writer)?; match self.version { OutputFeaturesVersion::V0 => (), - OutputFeaturesVersion::V1 => { + _ => { OutputFeatures::consensus_encode_recovery_byte(self.recovery_byte, writer)?; }, } @@ -335,7 +347,7 @@ impl ConsensusEncoding for OutputFeatures { self.metadata.consensus_encode(writer)?; match self.version { OutputFeaturesVersion::V0 => (), - OutputFeaturesVersion::V1 => { + _ => { self.committee_definition.consensus_encode(writer)?; }, } @@ -354,7 +366,7 @@ impl ConsensusDecoding for OutputFeatures { let flags = OutputFlags::consensus_decode(reader)?; let recovery_byte = match version { OutputFeaturesVersion::V0 => 0, - OutputFeaturesVersion::V1 => OutputFeatures::consensus_decode_recovery_byte(reader)?, + _ => OutputFeatures::consensus_decode_recovery_byte(reader)?, }; let parent_public_key = as ConsensusDecoding>::consensus_decode(reader)?; const MAX_UNIQUE_ID_SIZE: usize = 256; @@ -368,9 +380,7 @@ impl ConsensusDecoding for OutputFeatures { let metadata = as ConsensusDecoding>::consensus_decode(reader)?; let committee_definition = match version { OutputFeaturesVersion::V0 => None, - OutputFeaturesVersion::V1 => { - as ConsensusDecoding>::consensus_decode(reader)? - }, + _ => as ConsensusDecoding>::consensus_decode(reader)?, }; Ok(Self { version, @@ -446,7 +456,12 @@ mod test { RequirementsForConstitutionChange, SideChainConsensus, }, + vec_into_fixed_string, ContractConstitution, + ContractDefinition, + ContractSpecification, + FunctionRef, + PublicFunction, }, }; @@ -457,7 +472,7 @@ mod test { maturity: u64::MAX, recovery_byte: match version { OutputFeaturesVersion::V0 => 0, - OutputFeaturesVersion::V1 => u8::MAX, + _ => u8::MAX, }, metadata: vec![1; 1024], unique_id: Some(vec![0u8; 256]), @@ -489,6 +504,30 @@ mod test { }, initial_reward: 100.into(), }), + definition: Some(ContractDefinition { + contract_id: FixedHash::zero(), + contract_name: vec_into_fixed_string("name".as_bytes().to_vec()), + contract_issuer: PublicKey::default(), + contract_spec: ContractSpecification { + runtime: vec_into_fixed_string("runtime".as_bytes().to_vec()), + public_functions: vec![ + PublicFunction { + name: vec_into_fixed_string("foo".as_bytes().to_vec()), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 0_u16, + }, + }, + PublicFunction { + name: vec_into_fixed_string("bar".as_bytes().to_vec()), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 1_u16, + }, + }, + ], + }, + }), }), // Deprecated parent_public_key: Some(PublicKey::default()), @@ -513,7 +552,7 @@ mod test { }), committee_definition: match version { OutputFeaturesVersion::V0 => None, - OutputFeaturesVersion::V1 => Some(CommitteeDefinitionFeatures { + _ => Some(CommitteeDefinitionFeatures { committee: iter::repeat_with(PublicKey::default).take(50).collect(), effective_sidechain_height: u64::MAX, }), diff --git a/base_layer/core/src/transactions/transaction_components/output_features_version.rs b/base_layer/core/src/transactions/transaction_components/output_features_version.rs index c949cdecdd..bf4c6c4715 100644 --- a/base_layer/core/src/transactions/transaction_components/output_features_version.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features_version.rs @@ -17,6 +17,7 @@ use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSi pub enum OutputFeaturesVersion { V0 = 0, V1 = 1, + V2 = 2, } impl OutputFeaturesVersion { @@ -36,6 +37,7 @@ impl TryFrom for OutputFeaturesVersion { match value { 0 => Ok(OutputFeaturesVersion::V0), 1 => Ok(OutputFeaturesVersion::V1), + 2 => Ok(OutputFeaturesVersion::V2), _ => Err("Unknown or unsupported OutputFeaturesVersion".into()), } } diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/contract_definition.rs b/base_layer/core/src/transactions/transaction_components/side_chain/contract_definition.rs new file mode 100644 index 0000000000..5ea4d7d454 --- /dev/null +++ b/base_layer/core/src/transactions/transaction_components/side_chain/contract_definition.rs @@ -0,0 +1,275 @@ +// 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::{Error, Read, Write}; + +use integer_encoding::VarInt; +use serde::{Deserialize, Serialize}; +use tari_common_types::{ + array::copy_into_fixed_array_lossy, + types::{FixedHash, PublicKey}, +}; +use tari_utilities::Hashable; + +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusHashWriter, MaxSizeVec}; + +// Maximum number of functions allowed in a contract specification +const MAX_FUNCTIONS: usize = u16::MAX as usize; + +// Fixed length of all string fields in the contract definition +pub const STR_LEN: usize = 32; +type FixedString = [u8; STR_LEN]; + +pub fn vec_into_fixed_string(value: Vec) -> FixedString { + copy_into_fixed_array_lossy::<_, STR_LEN>(&value) +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, Hash)] +pub struct ContractDefinition { + pub contract_id: FixedHash, + pub contract_name: FixedString, + pub contract_issuer: PublicKey, + pub contract_spec: ContractSpecification, +} + +impl ContractDefinition { + pub fn new(contract_name: Vec, contract_issuer: PublicKey, contract_spec: ContractSpecification) -> Self { + let contract_name = vec_into_fixed_string(contract_name); + + let contract_id: FixedHash = ConsensusHashWriter::default() + .chain(&contract_name) + .chain(&contract_spec) + .finalize() + .into(); + + Self { + contract_id, + contract_name, + contract_issuer, + contract_spec, + } + } + + pub const fn str_byte_size() -> usize { + STR_LEN + } +} + +impl Hashable for ContractDefinition { + fn hash(&self) -> Vec { + ConsensusHashWriter::default().chain(self).finalize().to_vec() + } +} + +impl ConsensusEncoding for ContractDefinition { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.contract_id.consensus_encode(writer)?; + self.contract_name.consensus_encode(writer)?; + self.contract_issuer.consensus_encode(writer)?; + self.contract_spec.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for ContractDefinition { + fn consensus_encode_exact_size(&self) -> usize { + self.contract_id.consensus_encode_exact_size() + + STR_LEN + + self.contract_issuer.consensus_encode_exact_size() + + self.contract_spec.consensus_encode_exact_size() + } +} + +impl ConsensusDecoding for ContractDefinition { + fn consensus_decode(reader: &mut R) -> Result { + let contract_id = FixedHash::consensus_decode(reader)?; + let contract_name = FixedString::consensus_decode(reader)?; + let contract_issuer = PublicKey::consensus_decode(reader)?; + let contract_spec = ContractSpecification::consensus_decode(reader)?; + + Ok(Self { + contract_id, + contract_name, + contract_issuer, + contract_spec, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, Hash)] +pub struct ContractSpecification { + pub runtime: FixedString, + pub public_functions: Vec, +} + +impl Hashable for ContractSpecification { + fn hash(&self) -> Vec { + ConsensusHashWriter::default().chain(self).finalize().to_vec() + } +} + +impl ConsensusEncoding for ContractSpecification { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.runtime.consensus_encode(writer)?; + self.public_functions.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for ContractSpecification { + fn consensus_encode_exact_size(&self) -> usize { + let public_function_size = match self.public_functions.first() { + None => 0, + Some(function) => function.consensus_encode_exact_size(), + }; + + STR_LEN + self.public_functions.len().required_space() + self.public_functions.len() * public_function_size + } +} + +impl ConsensusDecoding for ContractSpecification { + fn consensus_decode(reader: &mut R) -> Result { + let runtime = FixedString::consensus_decode(reader)?; + let public_functions = MaxSizeVec::::consensus_decode(reader)?.into_vec(); + + Ok(Self { + runtime, + public_functions, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, Hash)] +pub struct PublicFunction { + pub name: FixedString, + pub function: FunctionRef, +} + +impl Hashable for PublicFunction { + fn hash(&self) -> Vec { + ConsensusHashWriter::default().chain(self).finalize().to_vec() + } +} + +impl ConsensusEncoding for PublicFunction { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.name.consensus_encode(writer)?; + self.function.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for PublicFunction { + fn consensus_encode_exact_size(&self) -> usize { + STR_LEN + self.function.consensus_encode_exact_size() + } +} + +impl ConsensusDecoding for PublicFunction { + fn consensus_decode(reader: &mut R) -> Result { + let name = FixedString::consensus_decode(reader)?; + let function = FunctionRef::consensus_decode(reader)?; + + Ok(Self { name, function }) + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, Hash)] +pub struct FunctionRef { + pub template_id: FixedHash, + pub function_id: u16, +} + +impl Hashable for FunctionRef { + fn hash(&self) -> Vec { + ConsensusHashWriter::default().chain(self).finalize().to_vec() + } +} + +impl ConsensusEncoding for FunctionRef { + fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { + self.template_id.consensus_encode(writer)?; + self.function_id.consensus_encode(writer)?; + + Ok(()) + } +} + +impl ConsensusEncodingSized for FunctionRef { + fn consensus_encode_exact_size(&self) -> usize { + self.template_id.consensus_encode_exact_size() + self.function_id.consensus_encode_exact_size() + } +} + +impl ConsensusDecoding for FunctionRef { + fn consensus_decode(reader: &mut R) -> Result { + let template_id = FixedHash::consensus_decode(reader)?; + let function_id = u16::consensus_decode(reader)?; + + Ok(Self { + template_id, + function_id, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::consensus::check_consensus_encoding_correctness; + + #[test] + fn it_encodes_and_decodes_correctly() { + let contract_name = str_to_fixed_string("contract_name"); + let contract_issuer = PublicKey::default(); + let contract_spec = ContractSpecification { + runtime: str_to_fixed_string("runtime value"), + public_functions: vec![ + PublicFunction { + name: str_to_fixed_string("foo"), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 0_u16, + }, + }, + PublicFunction { + name: str_to_fixed_string("bar"), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 1_u16, + }, + }, + ], + }; + + let contract_definition = ContractDefinition::new(contract_name.to_vec(), contract_issuer, contract_spec); + + check_consensus_encoding_correctness(contract_definition).unwrap(); + } + + fn str_to_fixed_string(s: &str) -> FixedString { + vec_into_fixed_string(s.as_bytes().to_vec()) + } +} diff --git a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs index fb3e37e630..b431606930 100644 --- a/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/side_chain/mod.rs @@ -31,8 +31,17 @@ pub use contract_constitution::{ SideChainConsensus, }; +mod contract_definition; +pub use contract_definition::{ + vec_into_fixed_string, + ContractDefinition, + ContractSpecification, + FunctionRef, + PublicFunction, +}; + mod committee_members; pub use committee_members::CommitteeMembers; mod sidechain_features; -pub use sidechain_features::SideChainFeatures; +pub use sidechain_features::{SideChainFeatures, SideChainFeaturesBuilder}; 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 d893835725..a4d7937ad1 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 @@ -25,6 +25,7 @@ use std::io::{Error, Read, Write}; use serde::{Deserialize, Serialize}; use tari_common_types::types::FixedHash; +use super::ContractDefinition; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, transactions::transaction_components::ContractConstitution, @@ -33,6 +34,7 @@ use crate::{ #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] pub struct SideChainFeatures { pub contract_id: FixedHash, + pub definition: Option, pub constitution: Option, } @@ -49,6 +51,7 @@ impl SideChainFeatures { impl ConsensusEncoding for SideChainFeatures { fn consensus_encode(&self, writer: &mut W) -> Result<(), Error> { self.contract_id.consensus_encode(writer)?; + self.definition.consensus_encode(writer)?; self.constitution.consensus_encode(writer)?; Ok(()) } @@ -60,6 +63,7 @@ impl ConsensusDecoding for SideChainFeatures { fn consensus_decode(reader: &mut R) -> Result { Ok(Self { contract_id: FixedHash::consensus_decode(reader)?, + definition: ConsensusDecoding::consensus_decode(reader)?, constitution: ConsensusDecoding::consensus_decode(reader)?, }) } @@ -74,11 +78,17 @@ impl SideChainFeaturesBuilder { Self { features: SideChainFeatures { contract_id, + definition: None, constitution: None, }, } } + pub fn with_contract_definition(mut self, contract_definition: ContractDefinition) -> Self { + self.features.definition = Some(contract_definition); + self + } + pub fn with_contract_constitution(mut self, contract_constitution: ContractConstitution) -> Self { self.features.constitution = Some(contract_constitution); self @@ -99,11 +109,15 @@ mod tests { use crate::{ consensus::check_consensus_encoding_correctness, transactions::transaction_components::{ + vec_into_fixed_string, CheckpointParameters, CommitteeMembers, ConstitutionChangeFlags, ConstitutionChangeRules, ContractAcceptanceRequirements, + ContractSpecification, + FunctionRef, + PublicFunction, RequirementsForConstitutionChange, SideChainConsensus, }, @@ -139,6 +153,30 @@ mod tests { }, initial_reward: 100.into(), }), + definition: Some(ContractDefinition { + contract_id: FixedHash::zero(), + contract_name: vec_into_fixed_string("name".as_bytes().to_vec()), + contract_issuer: PublicKey::default(), + contract_spec: ContractSpecification { + runtime: vec_into_fixed_string("runtime".as_bytes().to_vec()), + public_functions: vec![ + PublicFunction { + name: vec_into_fixed_string("foo".as_bytes().to_vec()), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 0_u16, + }, + }, + PublicFunction { + name: vec_into_fixed_string("bar".as_bytes().to_vec()), + function: FunctionRef { + template_id: FixedHash::zero(), + function_id: 1_u16, + }, + }, + ], + }, + }), }; check_consensus_encoding_correctness(subject).unwrap(); diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index 4c99530845..74edbb2e67 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -25,7 +25,13 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey, ASSET_CHECKPOINT_ID, COMMITTEE_DEFINITION_ID}, }; -use tari_core::transactions::transaction_components::{OutputFeatures, OutputFlags, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{ + ContractDefinition, + OutputFeatures, + OutputFlags, + TemplateParameter, + Transaction, +}; use crate::{ assets::Asset, @@ -271,6 +277,23 @@ impl AssetManager { Ok((tx_id, transaction)) } + + pub async fn create_contract_definition( + &mut self, + contract_definition: ContractDefinition, + ) -> Result<(TxId, Transaction), WalletError> { + let output = self + .output_manager + .create_output_with_features(0.into(), OutputFeatures::for_contract_definition(contract_definition)) + .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 c5fd22ad67..8ae7b5815b 100644 --- a/base_layer/wallet/src/assets/asset_manager_handle.rs +++ b/base_layer/wallet/src/assets/asset_manager_handle.rs @@ -24,7 +24,12 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey}, }; -use tari_core::transactions::transaction_components::{OutputFeatures, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{ + ContractDefinition, + OutputFeatures, + TemplateParameter, + Transaction, +}; use tari_service_framework::{reply_channel::SenderService, Service}; use crate::{ @@ -193,4 +198,23 @@ impl AssetManagerHandle { }), } } + + pub async fn create_contract_definition( + &mut self, + contract_definition: &ContractDefinition, + ) -> Result<(TxId, Transaction), WalletError> { + match self + .handle + .call(AssetManagerRequest::CreateContractDefinition { + contract_definition: Box::new(contract_definition.clone()), + }) + .await?? + { + AssetManagerResponse::CreateContractDefinition { transaction, tx_id } => Ok((tx_id, *transaction)), + _ => Err(WalletError::UnexpectedApiResponse { + method: "create_contract_definition".to_string(), + api: "AssetManagerService".to_string(), + }), + } + } } diff --git a/base_layer/wallet/src/assets/contract_definition_file_format.rs b/base_layer/wallet/src/assets/contract_definition_file_format.rs new file mode 100644 index 0000000000..06b7f524c3 --- /dev/null +++ b/base_layer/wallet/src/assets/contract_definition_file_format.rs @@ -0,0 +1,94 @@ +// 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 serde::{Deserialize, Serialize}; +use tari_common_types::types::{FixedHash, PublicKey}; +use tari_core::transactions::transaction_components::{ + vec_into_fixed_string, + ContractDefinition, + ContractSpecification, + FunctionRef, + PublicFunction, +}; +use tari_utilities::hex::Hex; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContractDefinitionFileFormat { + pub contract_name: String, + pub contract_issuer: PublicKey, + pub contract_spec: ContractSpecificationFileFormat, +} + +impl From for ContractDefinition { + fn from(value: ContractDefinitionFileFormat) -> Self { + let contract_name = value.contract_name.into_bytes(); + let contract_issuer = value.contract_issuer; + let contract_spec = value.contract_spec.into(); + + Self::new(contract_name, contract_issuer, contract_spec) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ContractSpecificationFileFormat { + pub runtime: String, + pub public_functions: Vec, +} + +impl From for ContractSpecification { + fn from(value: ContractSpecificationFileFormat) -> Self { + Self { + runtime: vec_into_fixed_string(value.runtime.into_bytes()), + public_functions: value.public_functions.into_iter().map(|f| f.into()).collect(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PublicFunctionFileFormat { + pub name: String, + pub function: FunctionRefFileFormat, +} + +impl From for PublicFunction { + fn from(value: PublicFunctionFileFormat) -> Self { + Self { + name: vec_into_fixed_string(value.name.into_bytes()), + function: value.function.into(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct FunctionRefFileFormat { + pub template_id: String, + pub function_id: u16, +} + +impl From for FunctionRef { + fn from(value: FunctionRefFileFormat) -> Self { + Self { + template_id: FixedHash::from_hex(&value.template_id).unwrap(), + function_id: value.function_id, + } + } +} 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 e3cccb74b4..c711087b28 100644 --- a/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs +++ b/base_layer/wallet/src/assets/infrastructure/asset_manager_service.rs @@ -82,6 +82,7 @@ impl AssetManagerService { Ok(()) } + #[allow(clippy::too_many_lines)] pub async fn handle_request(&mut self, request: AssetManagerRequest) -> Result { trace!(target: LOG_TARGET, "Handling Service API Request {:?}", request); match request { @@ -179,6 +180,13 @@ impl AssetManagerService { tx_id, }) }, + AssetManagerRequest::CreateContractDefinition { contract_definition } => { + let (tx_id, transaction) = self.manager.create_contract_definition(*contract_definition).await?; + Ok(AssetManagerResponse::CreateContractDefinition { + 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 434f3fe26b..277791068d 100644 --- a/base_layer/wallet/src/assets/infrastructure/mod.rs +++ b/base_layer/wallet/src/assets/infrastructure/mod.rs @@ -26,7 +26,12 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey}, }; -use tari_core::transactions::transaction_components::{OutputFeatures, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{ + ContractDefinition, + OutputFeatures, + TemplateParameter, + Transaction, +}; use crate::assets::Asset; @@ -68,6 +73,9 @@ pub enum AssetManagerRequest { effective_sidechain_height: u64, is_initial: bool, }, + CreateContractDefinition { + contract_definition: Box, + }, } pub enum AssetManagerResponse { @@ -78,4 +86,5 @@ pub enum AssetManagerResponse { CreateInitialCheckpoint { transaction: Box, tx_id: TxId }, CreateFollowOnCheckpoint { transaction: Box, tx_id: TxId }, CreateCommitteeDefinition { transaction: Box, tx_id: TxId }, + CreateContractDefinition { transaction: Box, tx_id: TxId }, } diff --git a/base_layer/wallet/src/assets/mod.rs b/base_layer/wallet/src/assets/mod.rs index b855caf1da..0d75edee01 100644 --- a/base_layer/wallet/src/assets/mod.rs +++ b/base_layer/wallet/src/assets/mod.rs @@ -30,3 +30,6 @@ pub use asset::Asset; mod asset_manager_handle; pub use asset_manager_handle::AssetManagerHandle; pub(crate) mod infrastructure; + +mod contract_definition_file_format; +pub use contract_definition_file_format::ContractDefinitionFileFormat; diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index 6d8d44daa6..581d1ac7b5 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -151,3 +151,15 @@ Feature: Wallet CLI And I create committee definition for asset on wallet WALLET via command line And mining node MINE mines 1 blocks Then WALLET is connected to BASE + + @dan_layer + Scenario: As a user I want to publish a contract definition 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 create a contract definition from file "fixtures/contract_definition.json" on wallet WALLET via command line + And mining node MINE mines 4 blocks + Then wallet WALLET has at least 1 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 c7dff3cf10..82dbbb3be3 100644 --- a/integration_tests/features/support/wallet_cli_steps.js +++ b/integration_tests/features/support/wallet_cli_steps.js @@ -23,6 +23,8 @@ const { Given, Then, When } = require("@cucumber/cucumber"); const { expect } = require("chai"); const { waitFor, sleep, byteArrayToHex } = require("../../helpers/util"); +const path = require("path"); + Given( /I change the password of wallet (.*) to (.*) via command line/, async function (name, newPassword) { @@ -297,3 +299,17 @@ Then( expect(output.buffer).to.have.string("Minting tokens for asset"); } ); + +Then( + "I create a contract definition 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, + `publish-contract-definition --json-file ${absolute_path}` + ); + console.log(output.buffer); + } +); diff --git a/integration_tests/fixtures/contract_definition.json b/integration_tests/fixtures/contract_definition.json new file mode 100644 index 0000000000..6edef97377 --- /dev/null +++ b/integration_tests/fixtures/contract_definition.json @@ -0,0 +1,23 @@ +{ + "contract_name": "contract_name value", + "contract_issuer": "dcba46de8c34824280a95f7f9e1e25684bc544aa911223106288c60d97518b7e", + "contract_spec": { + "runtime": "runtime value", + "public_functions": [ + { + "name": "foo", + "function": { + "template_id": "dcba46de8c34824280a95f7f9e1e25684bc544aa911223106288c60d97518b7e", + "function_id": 1 + } + }, + { + "name": "bar", + "function": { + "template_id": "dcba46de8c34824280a95f7f9e1e25684bc544aa911223106288c60d97518b7e", + "function_id": 2 + } + } + ] + } +}