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 + } + } + ] + } +}