diff --git a/Cargo.lock b/Cargo.lock index e7d4e58760..1e01be90be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1616,9 +1616,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.11" +version = "4.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" dependencies = [ "clap_builder", "clap_derive 4.4.7", @@ -1626,9 +1626,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" dependencies = [ "anstream", "anstyle", @@ -2134,7 +2134,7 @@ dependencies = [ "anyhow", "async-trait", "atty", - "clap 4.4.11", + "clap 4.4.13", "console", "cucumber-codegen", "cucumber-expressions", @@ -9705,7 +9705,7 @@ dependencies = [ "axum", "axum-jrpc", "chrono", - "clap 4.4.11", + "clap 4.4.13", "dirs", "jsonwebtoken", "log", @@ -10550,7 +10550,7 @@ dependencies = [ "anyhow", "bincode 2.0.0-rc.3", "bytes 1.5.0", - "clap 4.4.11", + "clap 4.4.13", "once_cell", "rand", "rayon", @@ -10559,6 +10559,7 @@ dependencies = [ "tari_template_builtin", "tari_template_lib", "tari_transaction", + "tari_transaction_manifest", ] [[package]] @@ -10566,7 +10567,8 @@ name = "transaction_submitter" version = "0.3.0" dependencies = [ "anyhow", - "clap 4.4.11", + "clap 4.4.13", + "tari_transaction", "tari_validator_node_client", "tokio", "transaction_generator", diff --git a/applications/tari_dan_wallet_cli/src/command/transaction.rs b/applications/tari_dan_wallet_cli/src/command/transaction.rs index 911c7af250..2a19937e39 100644 --- a/applications/tari_dan_wallet_cli/src/command/transaction.rs +++ b/applications/tari_dan_wallet_cli/src/command/transaction.rs @@ -294,12 +294,16 @@ async fn handle_submit_manifest( let request = TransactionSubmitRequest { signing_key_index: None, - fee_instructions: vec![Instruction::CallMethod { - component_address: fee_account.address.as_component_address().unwrap(), - method: "pay_fee".to_string(), - args: args![Amount::try_from(common.max_fee.unwrap_or(1000))?], - }], - instructions, + fee_instructions: instructions + .fee_instructions + .into_iter() + .chain(vec![Instruction::CallMethod { + component_address: fee_account.address.as_component_address().unwrap(), + method: "pay_fee".to_string(), + args: args![Amount::try_from(common.max_fee.unwrap_or(1000))?], + }]) + .collect(), + instructions: instructions.instructions, inputs: common.inputs, override_inputs: common.override_inputs.unwrap_or_default(), is_dry_run: common.dry_run, diff --git a/applications/tari_dan_wallet_daemon/src/cli.rs b/applications/tari_dan_wallet_daemon/src/cli.rs index 2fa1aefc9e..f46ffc6be6 100644 --- a/applications/tari_dan_wallet_daemon/src/cli.rs +++ b/applications/tari_dan_wallet_daemon/src/cli.rs @@ -40,6 +40,8 @@ pub struct Cli { pub signaling_server_address: Option, #[clap(long, alias = "indexer-url")] pub indexer_node_json_rpc_url: Option, + #[clap(long)] + pub derive_secret: Option, } impl Cli { diff --git a/applications/tari_dan_wallet_daemon/src/lib.rs b/applications/tari_dan_wallet_daemon/src/lib.rs index bc0f6579b1..b901fc50a7 100644 --- a/applications/tari_dan_wallet_daemon/src/lib.rs +++ b/applications/tari_dan_wallet_daemon/src/lib.rs @@ -24,7 +24,7 @@ pub mod cli; pub mod config; mod handlers; mod http_ui; -mod indexer_jrpc_impl; +pub mod indexer_jrpc_impl; mod jrpc_server; mod notify; mod services; @@ -67,24 +67,7 @@ pub async fn run_tari_dan_wallet_daemon( // Uncomment to enable tokio tracing via tokio-console // console_subscriber::init(); - let store = SqliteWalletStore::try_open(config.common.base_path.join("data/wallet.sqlite"))?; - store.run_migrations()?; - - let sdk_config = WalletSdkConfig { - // TODO: Configure - password: None, - indexer_jrpc_endpoint: config.dan_wallet_daemon.indexer_node_json_rpc_url, - jwt_expiry: config.dan_wallet_daemon.jwt_expiry.unwrap(), - jwt_secret_key: config.dan_wallet_daemon.jwt_secret_key.unwrap(), - }; - let config_api = ConfigApi::new(&store); - let indexer_jrpc_endpoint = if let Some(indexer_url) = config_api.get(ConfigKey::IndexerUrl).optional()? { - indexer_url - } else { - sdk_config.indexer_jrpc_endpoint.clone() - }; - let indexer = IndexerJsonRpcNetworkInterface::new(indexer_jrpc_endpoint); - let wallet_sdk = DanWalletSdk::initialize(store, indexer, sdk_config)?; + let wallet_sdk = initialize_wallet_sdk(&config)?; wallet_sdk .key_manager_api() .get_or_create_initial(key_manager::TRANSACTION_BRANCH)?; @@ -127,3 +110,27 @@ pub async fn run_tari_dan_wallet_daemon( } Ok(()) } + +pub fn initialize_wallet_sdk( + config: &ApplicationConfig, +) -> anyhow::Result> { + let store = SqliteWalletStore::try_open(config.common.base_path.join("data/wallet.sqlite"))?; + store.run_migrations()?; + + let sdk_config = WalletSdkConfig { + // TODO: Configure + password: None, + indexer_jrpc_endpoint: config.dan_wallet_daemon.indexer_node_json_rpc_url.clone(), + jwt_expiry: config.dan_wallet_daemon.jwt_expiry.unwrap(), + jwt_secret_key: config.dan_wallet_daemon.jwt_secret_key.clone().unwrap(), + }; + let config_api = ConfigApi::new(&store); + let indexer_jrpc_endpoint = if let Some(indexer_url) = config_api.get(ConfigKey::IndexerUrl).optional()? { + indexer_url + } else { + sdk_config.indexer_jrpc_endpoint.clone() + }; + let indexer = IndexerJsonRpcNetworkInterface::new(indexer_jrpc_endpoint); + let wallet_sdk = DanWalletSdk::initialize(store, indexer, sdk_config)?; + Ok(wallet_sdk) +} diff --git a/applications/tari_dan_wallet_daemon/src/main.rs b/applications/tari_dan_wallet_daemon/src/main.rs index 402d067df7..1b085e4a29 100644 --- a/applications/tari_dan_wallet_daemon/src/main.rs +++ b/applications/tari_dan_wallet_daemon/src/main.rs @@ -23,7 +23,9 @@ use std::{fs, panic, process}; use tari_common::{initialize_logging, load_configuration}; -use tari_dan_wallet_daemon::{cli::Cli, config::ApplicationConfig, run_tari_dan_wallet_daemon}; +use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; +use tari_dan_wallet_daemon::{cli::Cli, config::ApplicationConfig, initialize_wallet_sdk, run_tari_dan_wallet_daemon}; +use tari_dan_wallet_sdk::apis::key_manager; use tari_shutdown::Shutdown; #[tokio::main] @@ -37,10 +39,21 @@ async fn main() -> Result<(), anyhow::Error> { })); let cli = Cli::init(); + let config_path = cli.common.config_path(); let cfg = load_configuration(config_path, true, &cli)?; let config = ApplicationConfig::load_from(&cfg)?; + if let Some(index) = cli.derive_secret { + let sdk = initialize_wallet_sdk(&config)?; + let secret = sdk + .key_manager_api() + .derive_key(key_manager::TRANSACTION_BRANCH, index)?; + println!("Secret: {}", secret.key.reveal()); + println!("Public key: {}", RistrettoPublicKey::from_secret_key(&secret.key)); + return Ok(()); + } + // Remove the file if it was left behind by a previous run let _file = fs::remove_file(config.common.base_path.join("pid")); diff --git a/applications/tari_validator_node/src/json_rpc/handlers.rs b/applications/tari_validator_node/src/json_rpc/handlers.rs index 94e8dff8a6..395c6a5179 100644 --- a/applications/tari_validator_node/src/json_rpc/handlers.rs +++ b/applications/tari_validator_node/src/json_rpc/handlers.rs @@ -297,7 +297,7 @@ impl JsonRpcHandlers { let request: GetTransactionResultRequest = value.parse_params()?; let mut tx = self.state_store.create_read_tx().map_err(internal_error(answer_id))?; - let executed = ExecutedTransaction::get(&mut tx, &request.transaction_id) + let transaction = TransactionRecord::get(&mut tx, &request.transaction_id) .optional() .map_err(internal_error(answer_id))? .ok_or_else(|| { @@ -312,8 +312,9 @@ impl JsonRpcHandlers { })?; let response = GetTransactionResultResponse { - is_finalized: executed.is_finalized(), - result: executed.into_final_result(), + is_finalized: transaction.is_finalized(), + execution_time: transaction.execution_time(), + result: transaction.into_final_result(), }; Ok(JsonRpcResponse::success(answer_id, response)) } diff --git a/applications/tari_validator_node_cli/src/command/manifest.rs b/applications/tari_validator_node_cli/src/command/manifest.rs index 4094ed2fba..3f5a5e75bd 100644 --- a/applications/tari_validator_node_cli/src/command/manifest.rs +++ b/applications/tari_validator_node_cli/src/command/manifest.rs @@ -39,7 +39,8 @@ impl ManifestSubcommand { let contents = get_contents(args.manifest)?; let instructions = tari_transaction_manifest::parse_manifest(&contents, Default::default())?; // TODO: improve output - println!("Instructions: {:#?}", instructions); + println!("Instructions: {:#?}", instructions.instructions); + println!("Fee Instructions: {:#?}", instructions.fee_instructions); }, ManifestSubcommand::New(args) => { let mut out_stream = if let Some(ref path) = args.manifest { diff --git a/applications/tari_validator_node_cli/src/command/transaction.rs b/applications/tari_validator_node_cli/src/command/transaction.rs index bf317e1899..db0682d362 100644 --- a/applications/tari_validator_node_cli/src/command/transaction.rs +++ b/applications/tari_validator_node_cli/src/command/transaction.rs @@ -208,7 +208,7 @@ async fn handle_submit_manifest( ) -> Result { let contents = std::fs::read_to_string(&args.manifest).map_err(|e| anyhow!("Failed to read manifest: {}", e))?; let instructions = parse_manifest(&contents, manifest::parse_globals(args.input_variables)?)?; - submit_transaction(instructions, args.common, base_dir, client).await + submit_transaction(instructions.instructions, args.common, base_dir, client).await } pub async fn submit_transaction( diff --git a/clients/validator_node_client/src/types.rs b/clients/validator_node_client/src/types.rs index 7c36b7d4d9..0f21bfcbbd 100644 --- a/clients/validator_node_client/src/types.rs +++ b/clients/validator_node_client/src/types.rs @@ -173,6 +173,7 @@ pub struct GetTransactionResultRequest { pub struct GetTransactionResultResponse { pub result: Option, pub is_finalized: bool, + pub execution_time: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/dan_layer/engine/tests/templates/tariswap/src/lib.rs b/dan_layer/engine/tests/templates/tariswap/src/lib.rs index ec40eaf6e6..3054ffb724 100644 --- a/dan_layer/engine/tests/templates/tariswap/src/lib.rs +++ b/dan_layer/engine/tests/templates/tariswap/src/lib.rs @@ -44,7 +44,7 @@ mod tariswap { Self::check_resource_is_fungible(b_addr); // the fee represents a percentage, so it must be between 0 and 100 - let valid_fee_range = 0..1000; + let valid_fee_range = 0..100; assert!(valid_fee_range.contains(&fee), "Invalid fee {}", fee); // create the vaults to store the funds @@ -223,9 +223,10 @@ mod tariswap { } fn check_resource_is_fungible(resource: ResourceAddress) { + let resource_type = ResourceManager::get(resource).resource_type(); assert!( - ResourceManager::get(resource).resource_type() == ResourceType::Fungible, - "Resource {} is not fungible", + matches!(resource_type, ResourceType::Fungible | ResourceType::Confidential), + "Resource {} is not fungible nor confidential", resource ); } diff --git a/dan_layer/storage/src/consensus_models/transaction.rs b/dan_layer/storage/src/consensus_models/transaction.rs index 55392b6777..bf5f85e49d 100644 --- a/dan_layer/storage/src/consensus_models/transaction.rs +++ b/dan_layer/storage/src/consensus_models/transaction.rs @@ -96,6 +96,10 @@ impl TransactionRecord { self.execution_time } + pub fn is_finalized(&self) -> bool { + self.final_decision.is_some() + } + pub fn abort_details(&self) -> Option<&String> { self.abort_details.as_ref() } diff --git a/dan_layer/template_test_tooling/src/template_test.rs b/dan_layer/template_test_tooling/src/template_test.rs index ca0ca12db9..812bdf4552 100644 --- a/dan_layer/template_test_tooling/src/template_test.rs +++ b/dan_layer/template_test_tooling/src/template_test.rs @@ -559,7 +559,7 @@ impl TemplateTest { variables.into_iter().map(|(a, b)| (a.to_string(), b)).collect(), ) .unwrap(); - self.execute_and_commit(instructions, proofs) + self.execute_and_commit(instructions.instructions, proofs) } pub fn print_state(&self) { diff --git a/dan_layer/transaction_manifest/src/ast.rs b/dan_layer/transaction_manifest/src/ast.rs index d561e95818..2e423c0de7 100644 --- a/dan_layer/transaction_manifest/src/ast.rs +++ b/dan_layer/transaction_manifest/src/ast.rs @@ -25,17 +25,17 @@ use syn::{ Result, }; -use crate::parser::{ManifestIntent, ManifestParser}; +use crate::parser::{ManifestParser, ParsedManifest}; #[derive(Debug, Clone)] pub struct ManifestAst { - pub intents: Vec, + pub parsed: ParsedManifest, } impl Parse for ManifestAst { fn parse(input: ParseStream) -> Result { let parser = ManifestParser::new(); - let intents = parser.parse(input)?; - Ok(Self { intents }) + let parsed = parser.parse(input)?; + Ok(Self { parsed }) } } diff --git a/dan_layer/transaction_manifest/src/generator.rs b/dan_layer/transaction_manifest/src/generator.rs index b9aeed7c54..6606590649 100644 --- a/dan_layer/transaction_manifest/src/generator.rs +++ b/dan_layer/transaction_manifest/src/generator.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; +use proc_macro2::Ident; use syn::Lit; use tari_engine_types::{instruction::Instruction, substate::SubstateAddress, TemplateAddress}; use tari_template_lib::{ @@ -15,11 +16,12 @@ use crate::{ ast::ManifestAst, error::ManifestError, parser::{InvokeIntent, ManifestIntent, ManifestLiteral, SpecialLiteral}, + ManifestInstructions, ManifestValue, }; pub struct ManifestInstructionGenerator { - imported_templates: HashMap, + imported_templates: HashMap, global_aliases: HashMap, globals: HashMap, variables: HashSet, @@ -35,24 +37,32 @@ impl ManifestInstructionGenerator { } } - pub fn generate_instructions(&mut self, ast: ManifestAst) -> Result, ManifestError> { - let mut instructions = Vec::with_capacity(ast.intents.len()); - for intent in ast.intents { + pub fn generate_instructions(&mut self, ast: ManifestAst) -> Result { + self.imported_templates = ast + .parsed + .defines + .into_iter() + .map(|import| (import.alias, import.template_address)) + .collect(); + + let mut instructions = Vec::with_capacity(ast.parsed.instruction_intents.len()); + for intent in ast.parsed.instruction_intents { instructions.extend(self.translate_intent(intent)?); } - Ok(instructions) + let mut fee_instructions = Vec::with_capacity(ast.parsed.fee_instruction_intents.len()); + for intent in ast.parsed.fee_instruction_intents { + fee_instructions.extend(self.translate_intent(intent)?); + } + + Ok(ManifestInstructions { + instructions, + fee_instructions, + }) } fn translate_intent(&mut self, intent: ManifestIntent) -> Result, ManifestError> { match intent { - ManifestIntent::DefineTemplate { - template_address, - alias, - } => { - self.imported_templates.insert(alias.to_string(), template_address); - Ok(vec![]) - }, ManifestIntent::InvokeTemplate(InvokeIntent { output_variable, template_variable, @@ -64,7 +74,7 @@ impl ManifestInstructionGenerator { .as_ref() .expect("AST parse should have failed: no template ident for TemplateInvoke statement"); let mut instructions = vec![Instruction::CallFunction { - template_address: self.get_imported_template(&template_ident.to_string())?, + template_address: self.get_imported_template(template_ident)?, function: function_name.to_string(), args: self.process_args(arguments)?, }]; @@ -173,7 +183,7 @@ impl ManifestInstructionGenerator { .collect() } - fn get_imported_template(&self, name: &str) -> Result { + fn get_imported_template(&self, name: &Ident) -> Result { self.imported_templates .get(name) .copied() diff --git a/dan_layer/transaction_manifest/src/lib.rs b/dan_layer/transaction_manifest/src/lib.rs index f03b5eaf29..af923401d5 100644 --- a/dan_layer/transaction_manifest/src/lib.rs +++ b/dan_layer/transaction_manifest/src/lib.rs @@ -36,9 +36,17 @@ mod generator; mod parser; mod value; -pub fn parse_manifest(input: &str, globals: HashMap) -> Result, ManifestError> { +pub fn parse_manifest( + input: &str, + globals: HashMap, +) -> Result { let tokens = TokenStream::from_str(input).map_err(|e| ManifestError::LexError(e.to_string()))?; let ast = parse2::(tokens)?; ManifestInstructionGenerator::new(globals).generate_instructions(ast) } + +pub struct ManifestInstructions { + pub instructions: Vec, + pub fee_instructions: Vec, +} diff --git a/dan_layer/transaction_manifest/src/parser.rs b/dan_layer/transaction_manifest/src/parser.rs index 73dd79ed8c..27be256457 100644 --- a/dan_layer/transaction_manifest/src/parser.rs +++ b/dan_layer/transaction_manifest/src/parser.rs @@ -24,6 +24,7 @@ use syn::{ Pat, PatIdent, Path, + Signature, Stmt, UseTree, }; @@ -33,16 +34,18 @@ use tari_template_lib::args::LogLevel; #[derive(Debug, Clone)] pub enum ManifestIntent { - DefineTemplate { - template_address: TemplateAddress, - alias: Ident, - }, InvokeTemplate(InvokeIntent), InvokeComponent(InvokeIntent), AssignInput(AssignInputStmt), Log(LogIntent), } +#[derive(Debug, Clone)] +pub struct ManifestImport { + pub template_address: TemplateAddress, + pub alias: Ident, +} + #[derive(Debug, Clone)] pub struct InvokeIntent { pub output_variable: Option, @@ -79,17 +82,27 @@ pub enum SpecialLiteral { pub struct ManifestParser; +#[derive(Debug, Clone)] +pub struct ParsedManifest { + pub defines: Vec, + pub instruction_intents: Vec, + pub fee_instruction_intents: Vec, +} + impl ManifestParser { pub fn new() -> Self { Self } - pub fn parse(&self, input: ParseStream) -> Result, syn::Error> { - let mut statements = vec![]; - statements.push(ManifestIntent::DefineTemplate { + pub fn parse(&self, input: ParseStream) -> Result { + let mut instruction_intents = vec![]; + let mut fee_instruction_intents = vec![]; + let mut defines = vec![]; + defines.push(ManifestImport { template_address: *ACCOUNT_TEMPLATE_ADDRESS, alias: Ident::new("Account", proc_macro2::Span::call_site()), }); + for stmt in Block::parse_within(input)? { match stmt { // use template_hash as TemplateName; @@ -103,13 +116,29 @@ impl ManifestParser { .and_then(|(_, s)| TemplateAddress::from_hex(s).ok()) .ok_or_else(|| syn::Error::new_spanned(rename.clone(), "Invalid template address"))?; - statements.push(ManifestIntent::DefineTemplate { + defines.push(ManifestImport { template_address, alias: rename.rename, }); }, - Stmt::Item(Item::Fn(ItemFn { block, .. })) => { - statements.extend(self.parse_block(*block)?); + Stmt::Item(Item::Fn(ItemFn { + block, + sig: Signature { ident, .. }, + .. + })) => { + if ident == "fee_main" { + fee_instruction_intents.extend(self.parse_block(*block)?); + } else if ident == "main" { + instruction_intents.extend(self.parse_block(*block)?); + } else { + return Err(syn::Error::new_spanned( + block, + format!( + "Invalid function declaration {}. Only main or fee_main are allowed.", + ident + ), + )); + } }, _ => { return Err(syn::Error::new_spanned( @@ -120,7 +149,11 @@ impl ManifestParser { } } - Ok(statements) + Ok(ParsedManifest { + defines, + instruction_intents, + fee_instruction_intents, + }) } fn parse_block(&self, block: Block) -> Result, syn::Error> { @@ -129,23 +162,6 @@ impl ManifestParser { pub fn parse_stmt(&self, stmt: Stmt) -> Result { match stmt { - // use template_hash as TemplateName; - Stmt::Item(Item::Use(ItemUse { - tree: UseTree::Rename(rename), - .. - })) => { - let template_id = rename.ident.to_string(); - let template_address = template_id - .split_once('_') - .and_then(|(_, s)| TemplateAddress::from_hex(s).ok()) - .ok_or_else(|| syn::Error::new_spanned(rename.clone(), "Invalid template address"))?; - - Ok(ManifestIntent::DefineTemplate { - template_address, - alias: rename.rename, - }) - }, - // let variable_name = TemplateName::function_name(arg1, arg2); Stmt::Local(local) => self.handle_local(local), // component.function_name(arg1, arg2); Stmt::Semi(expr, _) => self.handle_semi_expr(expr), @@ -295,7 +311,7 @@ impl ManifestParser { fn assignment_from_macro(var_name: Ident, mac: &Ident, tokens: TokenStream) -> Result { match mac.to_string().as_str() { - "global" | "var" => Ok(ManifestIntent::AssignInput(AssignInputStmt { + "global" | "var" | "arg" => Ok(ManifestIntent::AssignInput(AssignInputStmt { variable_name: var_name, global_variable_name: parse2(tokens)?, })), diff --git a/dan_layer/transaction_manifest/tests/parser.rs b/dan_layer/transaction_manifest/tests/parser.rs index 19d58a7263..21de55df89 100644 --- a/dan_layer/transaction_manifest/tests/parser.rs +++ b/dan_layer/transaction_manifest/tests/parser.rs @@ -27,7 +27,7 @@ use tari_template_lib::{ args, models::{Amount, ComponentAddress, ResourceAddress, TemplateAddress}, }; -use tari_transaction_manifest::parse_manifest; +use tari_transaction_manifest::{parse_manifest, ManifestInstructions}; #[test] #[allow(clippy::too_many_lines)] @@ -58,7 +58,10 @@ fn manifest_smoke_test() { SubstateAddress::Resource(xtr_resource).into(), ), ]); - let instructions = parse_manifest(&input, globals).unwrap(); + let ManifestInstructions { + instructions, + fee_instructions, + } = parse_manifest(&input, globals).unwrap(); let expected = vec![ Instruction::CallFunction { @@ -104,4 +107,5 @@ fn manifest_smoke_test() { ]; assert_eq!(instructions, expected); + assert_eq!(fee_instructions, vec![]); } diff --git a/integration_tests/src/validator_node_cli.rs b/integration_tests/src/validator_node_cli.rs index e6dad0d42b..8a19d02705 100644 --- a/integration_tests/src/validator_node_cli.rs +++ b/integration_tests/src/validator_node_cli.rs @@ -348,7 +348,7 @@ pub async fn submit_manifest( account_template_address: None, dry_run: false, }; - let resp = submit_transaction(instructions, args, data_dir, &mut client) + let resp = submit_transaction(instructions.instructions, args, data_dir, &mut client) .await .unwrap(); diff --git a/integration_tests/src/wallet_daemon_cli.rs b/integration_tests/src/wallet_daemon_cli.rs index fbefe7d86b..340439b0fd 100644 --- a/integration_tests/src/wallet_daemon_cli.rs +++ b/integration_tests/src/wallet_daemon_cli.rs @@ -450,7 +450,7 @@ pub async fn submit_manifest_with_signing_keys( let instructions = parse_manifest(&manifest_content, globals).unwrap(); let transaction_submit_req = TransactionSubmitRequest { signing_key_index: Some(account.key_index), - instructions, + instructions: instructions.instructions, fee_instructions: vec![], override_inputs: false, is_dry_run: false, @@ -522,7 +522,7 @@ pub async fn submit_manifest( let transaction_submit_req = TransactionSubmitRequest { signing_key_index: None, - instructions, + instructions: instructions.instructions, fee_instructions: vec![], override_inputs: false, is_dry_run: false, diff --git a/utilities/transaction_generator/Cargo.toml b/utilities/transaction_generator/Cargo.toml index c57cf67136..43e04f8ac4 100644 --- a/utilities/transaction_generator/Cargo.toml +++ b/utilities/transaction_generator/Cargo.toml @@ -11,12 +11,13 @@ tari_template_lib = { workspace = true } tari_transaction = { workspace = true } tari_engine_types = { workspace = true } tari_template_builtin = { workspace = true } +tari_transaction_manifest = { workspace = true } tari_crypto = { workspace = true } anyhow = { workspace = true } bincode = { workspace = true, features = ["serde"] } bytes = { workspace = true } -# if we set clap version 4 in the workspace it would break other crates +# if we set clap version 4 in the workspace it would break other crates clap = { version = "4.3.21", features = ["derive"] } rayon = { workspace = true } rand = { workspace = true } diff --git a/utilities/transaction_generator/manifests/create_free_coins.rs b/utilities/transaction_generator/manifests/create_free_coins.rs new file mode 100644 index 0000000000..7736ba9e74 --- /dev/null +++ b/utilities/transaction_generator/manifests/create_free_coins.rs @@ -0,0 +1,14 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +// TODO: This does not work currently + +fn fee_main() { + let owner = arg!["owner"]; + let free_coins = create_free_coins!(Amount(1000), None); + let account = allocate_component_address!(); + Account::create_advanced(owner, free_coins, account); + account.pay_fee(Amount(1000)); +} + +fn main() {} diff --git a/utilities/transaction_generator/manifests/tariswap_do_swap.rs b/utilities/transaction_generator/manifests/tariswap_do_swap.rs new file mode 100644 index 0000000000..d2d763a7da --- /dev/null +++ b/utilities/transaction_generator/manifests/tariswap_do_swap.rs @@ -0,0 +1,21 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +fn fee_main() { + let in_account = arg!["in_account"]; + in_account.pay_fee(Amount(1000)); +} + +fn main() { + let swap_component = arg!["swap_component"]; + let in_account = arg!["in_account"]; + let out_account = arg!["out_account"]; + let amt = arg!["amt"]; + + let token_a = arg!["token_a"]; + let token_b = arg!["token_b"]; + + let in_bucket = in_account.withdraw(token_a, amt); + let out_bucket = swap_component.swap(in_bucket, token_b); + out_account.deposit(out_bucket); +} diff --git a/utilities/transaction_generator/src/cli.rs b/utilities/transaction_generator/src/cli.rs index 1aec5e798c..366d17d743 100644 --- a/utilities/transaction_generator/src/cli.rs +++ b/utilities/transaction_generator/src/cli.rs @@ -33,6 +33,12 @@ pub struct WriteArgs { pub output_file: PathBuf, #[clap(long)] pub overwrite: bool, + #[clap(long, short = 'm')] + pub manifest: Option, + #[clap(long, short = 'g', alias = "global")] + pub manifest_globals: Vec, + #[clap(long, short = 'k', alias = "signer")] + pub signer_secret_key: Option, } #[derive(Args, Debug)] pub struct ReadArgs { diff --git a/utilities/transaction_generator/src/lib.rs b/utilities/transaction_generator/src/lib.rs index bf152a44b3..315d714a18 100644 --- a/utilities/transaction_generator/src/lib.rs +++ b/utilities/transaction_generator/src/lib.rs @@ -1,8 +1,11 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +pub mod transaction_builders; mod transaction_reader; mod transaction_writer; pub use transaction_reader::*; pub use transaction_writer::*; + +pub type BoxedTransactionBuilder = Box tari_transaction::Transaction + Send + Sync + 'static>; diff --git a/utilities/transaction_generator/src/main.rs b/utilities/transaction_generator/src/main.rs index 86f60125cc..1328abbf33 100644 --- a/utilities/transaction_generator/src/main.rs +++ b/utilities/transaction_generator/src/main.rs @@ -4,20 +4,33 @@ mod cli; mod transaction_writer; -use std::io::{stdout, Seek, SeekFrom, Write}; +use std::{ + collections::HashMap, + fs, + io::{stdout, Seek, SeekFrom, Write}, +}; +use anyhow::anyhow; use cli::Cli; -use tari_template_lib::models::Amount; -use transaction_generator::{read_number_of_transactions, read_transactions}; +use rand::rngs::OsRng; +use tari_crypto::{keys::SecretKey, ristretto::RistrettoSecretKey, tari_utilities::hex::Hex}; +use tari_transaction_manifest::ManifestValue; +use transaction_generator::{ + read_number_of_transactions, + read_transactions, + transaction_builders::{free_coins, manifest}, + BoxedTransactionBuilder, +}; -use crate::{cli::SubCommand, transaction_writer::write_transactions}; +use crate::{ + cli::{SubCommand, WriteArgs}, + transaction_writer::write_transactions, +}; fn main() -> anyhow::Result<()> { let cli = Cli::init(); match cli.sub_command { SubCommand::Write(args) => { - let fee_amount = Amount(1000); - if !args.overwrite && args.output_file.exists() { anyhow::bail!("Output file {} already exists", args.output_file.display()); } @@ -26,9 +39,11 @@ fn main() -> anyhow::Result<()> { println!("Generating and writing {} transactions", args.num_transactions,); let mut file = std::fs::File::create(&args.output_file)?; + + let builder = get_transaction_builder(&args)?; write_transactions( args.num_transactions, - fee_amount, + builder, &|_| { print!("."); stdout().flush().unwrap() @@ -46,7 +61,7 @@ fn main() -> anyhow::Result<()> { ); }, SubCommand::Read(args) => { - let mut file = std::fs::File::open(args.input_file)?; + let mut file = fs::File::open(args.input_file)?; let num_transactions = read_number_of_transactions(&mut file)?; println!("Number of transactions: {}", num_transactions); @@ -61,3 +76,33 @@ fn main() -> anyhow::Result<()> { Ok(()) } + +fn get_transaction_builder(args: &WriteArgs) -> anyhow::Result { + match args.manifest.as_ref() { + Some(manifest) => { + let signer_key = args + .signer_secret_key + .as_ref() + .map(|s| RistrettoSecretKey::from_hex(s)) + .transpose() + .map_err(|_| anyhow!("Failed to parse secret"))? + .unwrap_or_else(|| RistrettoSecretKey::random(&mut OsRng)); + manifest::builder(signer_key, manifest, parse_globals(&args.manifest_globals)?) + }, + None => Ok(Box::new(free_coins::builder)), + } +} + +fn parse_globals(globals: &[String]) -> Result, anyhow::Error> { + let mut result = HashMap::with_capacity(globals.len()); + for global in globals { + let (name, value) = global + .split_once('=') + .ok_or_else(|| anyhow!("Invalid global: {}", global))?; + let value = value + .parse() + .map_err(|err| anyhow!("Failed to parse global '{}': {}", name, err))?; + result.insert(name.to_string(), value); + } + Ok(result) +} diff --git a/utilities/transaction_generator/src/transaction_builders/free_coins.rs b/utilities/transaction_generator/src/transaction_builders/free_coins.rs new file mode 100644 index 0000000000..64e6edff5d --- /dev/null +++ b/utilities/transaction_generator/src/transaction_builders/free_coins.rs @@ -0,0 +1,40 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use rand::rngs::OsRng; +use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; +use tari_engine_types::{component::new_component_address_from_parts, instruction::Instruction}; +use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; +use tari_template_lib::{ + args, + crypto::RistrettoPublicKeyBytes, + models::{Amount, NonFungibleAddress}, +}; +use tari_transaction::Transaction; + +pub fn builder(_: u64) -> Transaction { + let (signer_secret_key, signer_public_key) = RistrettoPublicKey::random_keypair(&mut OsRng); + + let owner_pk = RistrettoPublicKeyBytes::from_bytes(signer_public_key.as_bytes()).unwrap(); + let owner_token = NonFungibleAddress::from_public_key(owner_pk); + Transaction::builder() + .with_fee_instructions_builder(|builder| { + builder + .add_instruction(Instruction::CreateFreeTestCoins { + revealed_amount: Amount::new(1000), + output: None, + }) + .put_last_instruction_output_on_workspace(b"free_coins") + .call_function(*ACCOUNT_TEMPLATE_ADDRESS, "create_with_bucket", args![ + owner_token, + Workspace("free_coins") + ]) + .call_method( + new_component_address_from_parts(&ACCOUNT_TEMPLATE_ADDRESS, &owner_pk.into_array().into()), + "pay_fee", + args![Amount(1000)], + ) + }) + .sign(&signer_secret_key) + .build() +} diff --git a/utilities/transaction_generator/src/transaction_builders/manifest.rs b/utilities/transaction_generator/src/transaction_builders/manifest.rs new file mode 100644 index 0000000000..1c726b47c9 --- /dev/null +++ b/utilities/transaction_generator/src/transaction_builders/manifest.rs @@ -0,0 +1,26 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{collections::HashMap, fs, path::Path}; + +use tari_crypto::ristretto::RistrettoSecretKey; +use tari_transaction::Transaction; +use tari_transaction_manifest::ManifestValue; + +use crate::BoxedTransactionBuilder; + +pub fn builder>( + signer_secret_key: RistrettoSecretKey, + manifest: P, + globals: HashMap, +) -> anyhow::Result { + let contents = fs::read_to_string(manifest).unwrap(); + let instructions = tari_transaction_manifest::parse_manifest(&contents, globals)?; + Ok(Box::new(move |_| { + Transaction::builder() + .with_fee_instructions_builder(|builder| builder.with_instructions(instructions.fee_instructions.clone())) + .with_instructions(instructions.instructions.clone()) + .sign(&signer_secret_key) + .build() + })) +} diff --git a/utilities/transaction_generator/src/transaction_builders/mod.rs b/utilities/transaction_generator/src/transaction_builders/mod.rs new file mode 100644 index 0000000000..19c83fcdea --- /dev/null +++ b/utilities/transaction_generator/src/transaction_builders/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +pub mod free_coins; +pub mod manifest; diff --git a/utilities/transaction_generator/src/transaction_writer.rs b/utilities/transaction_generator/src/transaction_writer.rs index d5c3a74e1a..5c89e7da24 100644 --- a/utilities/transaction_generator/src/transaction_writer.rs +++ b/utilities/transaction_generator/src/transaction_writer.rs @@ -4,54 +4,21 @@ use std::{io::Write, sync::mpsc, thread}; use bytes::{BufMut, Bytes, BytesMut}; -use rand::rngs::OsRng; use rayon::iter::{ParallelBridge, ParallelIterator}; -use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey, tari_utilities::ByteArray}; -use tari_engine_types::{component::new_component_address_from_parts, instruction::Instruction}; -use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; -use tari_template_lib::{ - args, - crypto::RistrettoPublicKeyBytes, - models::{Amount, NonFungibleAddress}, -}; -use tari_transaction::Transaction; + +use crate::BoxedTransactionBuilder; pub fn write_transactions( num_transactions: u64, - fee_amount: Amount, + builder: BoxedTransactionBuilder, on_progress: &dyn Fn(usize), writer: &mut W, ) -> anyhow::Result<()> { let (sender, receiver) = mpsc::sync_channel(1000); thread::spawn(move || { - (0..num_transactions).par_bridge().for_each_with(sender, |sender, _| { - let (signer_secret_key, signer_public_key) = RistrettoPublicKey::random_keypair(&mut OsRng); - - let owner_pk = RistrettoPublicKeyBytes::from_bytes(signer_public_key.as_bytes()).unwrap(); - let owner_token = NonFungibleAddress::from_public_key(owner_pk); - - let transaction = Transaction::builder() - .with_fee_instructions_builder(|builder| { - builder - .add_instruction(Instruction::CreateFreeTestCoins { - revealed_amount: Amount::new(1000), - output: None, - }) - .put_last_instruction_output_on_workspace(b"free_coins") - .call_function(*ACCOUNT_TEMPLATE_ADDRESS, "create_with_bucket", args![ - owner_token, - Workspace("free_coins") - ]) - .call_method( - new_component_address_from_parts(&ACCOUNT_TEMPLATE_ADDRESS, &owner_pk.into_array().into()), - "pay_fee", - args![fee_amount], - ) - }) - .sign(&signer_secret_key) - .build(); - + (0..num_transactions).par_bridge().for_each_with(sender, |sender, i| { + let transaction = builder(i); let buf = bincode::serde::encode_to_vec(&transaction, bincode::config::standard()).unwrap(); let buf = Bytes::from(buf); let output = BytesMut::with_capacity(buf.len() + 2); diff --git a/utilities/transaction_submitter/Cargo.toml b/utilities/transaction_submitter/Cargo.toml index d769ec90c3..129c3dd570 100644 --- a/utilities/transaction_submitter/Cargo.toml +++ b/utilities/transaction_submitter/Cargo.toml @@ -11,8 +11,9 @@ license.workspace = true [dependencies] transaction_generator = { workspace = true } tari_validator_node_client = { workspace = true } +tari_transaction = { workspace = true } anyhow = { workspace = true } -# if we set clap version 4 in the workspace it would break other crates +# if we set clap version 4 in the workspace it would break other crates clap = "4.3.21" tokio = { workspace = true, default-features = true } diff --git a/utilities/transaction_submitter/src/main.rs b/utilities/transaction_submitter/src/main.rs index b2f17879ba..9db210a681 100644 --- a/utilities/transaction_submitter/src/main.rs +++ b/utilities/transaction_submitter/src/main.rs @@ -1,16 +1,19 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{ - cmp, - fs::File, - io::Write, - sync::{atomic::AtomicUsize, Arc}, -}; +use std::{cmp, fs::File, io::Write, time::Duration}; use anyhow::bail; -use tari_validator_node_client::{types::SubmitTransactionRequest, ValidatorNodeClient}; -use tokio::task; +use tari_transaction::TransactionId; +use tari_validator_node_client::{ + types::{GetTransactionResultRequest, SubmitTransactionRequest, SubmitTransactionResponse}, + ValidatorNodeClient, +}; +use tokio::{ + sync::mpsc, + task, + time::{sleep, timeout}, +}; use transaction_generator::{read_number_of_transactions, read_transactions}; use crate::{ @@ -26,14 +29,16 @@ async fn main() -> anyhow::Result<()> { let cli = Cli::init(); match cli.sub_command { SubCommand::StressTest(args) => { - stress_test(args).await?; + if let Some(summary) = stress_test(args).await? { + print_summary(&summary); + } }, } Ok(()) } -async fn stress_test(args: StressTestArgs) -> anyhow::Result<()> { +async fn stress_test(args: StressTestArgs) -> anyhow::Result> { let mut clients = Vec::with_capacity(args.jrpc_address.len()); for address in args.jrpc_address { let mut client = ValidatorNodeClient::connect(format!("http://{}/json_rpc", address))?; @@ -69,41 +74,42 @@ async fn stress_test(args: StressTestArgs) -> anyhow::Result<()> { std::io::stdin().read_line(&mut input)?; if !input.trim().eq_ignore_ascii_case("y") { println!("Aborting"); - return Ok(()); + return Ok(None); } } println!("⚠️ Submitting {} transactions", num_transactions); if num_transactions == 0 { - return Ok(()); + return Ok(Some(StressTestResultSummary::default())); } let transactions = read_transactions(File::open(args.transaction_file)?, args.skip_transactions.unwrap_or(0))?; let mut count = 0usize; let bounded_spawn = BoundedSpawn::new(clients.len() * 100); - let task_counter = Arc::new(AtomicUsize::new(0)); + let (submitted_tx, submitted_rx) = mpsc::unbounded_channel(); while let Ok(transaction) = transactions.recv() { let mut client = clients[count % clients.len()].clone(); - task_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + let submitted_tx = submitted_tx.clone(); // Bounded spawn prevents too many tasks from being spawned at once, to prevent opening too many sockets in the // OS. bounded_spawn - .spawn({ - let counter = task_counter.clone(); - async move { - if let Err(e) = client - .submit_transaction(SubmitTransactionRequest { - transaction, - is_dry_run: false, - }) - .await - { + .spawn(async move { + match client + .submit_transaction(SubmitTransactionRequest { + transaction, + is_dry_run: false, + }) + .await + { + Ok(SubmitTransactionResponse { transaction_id, .. }) => { + submitted_tx.send(transaction_id).unwrap(); + }, + Err(e) => { println!("Failed to submit transaction: {}", e); - } - counter.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + }, } }) .await; @@ -114,10 +120,178 @@ async fn stress_test(args: StressTestArgs) -> anyhow::Result<()> { } } - // Kinda hacky, but there will still be tasks waiting in the queue at this point so we can't quit yet - while task_counter.load(std::sync::atomic::Ordering::SeqCst) > 0 { - task::yield_now().await; + // Drop the remaining sender handle so that the result emitter ends when all results have been received + drop(submitted_tx); + + println!("Fetching results for {} transactions...", count); + let results = fetch_result_summary(clients, submitted_rx).await; + + Ok(Some(results)) +} + +async fn fetch_result_summary( + clients: Vec, + mut submitted_rx: mpsc::UnboundedReceiver, +) -> StressTestResultSummary { + let bounded_spawn = BoundedSpawn::new(clients.len()); + let mut count = 0; + let (results_tx, mut results_rx) = mpsc::channel::(10); + + // Result collector + let results_handle = task::spawn(async move { + let mut result = StressTestResultSummary::default(); + loop { + match timeout(Duration::from_secs(10), results_rx.recv()).await { + Ok(Some(tx)) => { + result.num_transactions += 1; + if tx.is_committed { + result.num_committed += 1; + result.num_up_substates += tx.num_up_substates; + result.num_down_substates += tx.num_down_substates; + result.slowest_execution_time = cmp::max(result.slowest_execution_time, tx.execution_time); + result.fastest_execution_time = cmp::min(result.fastest_execution_time, tx.execution_time); + result.total_execution_time += tx.execution_time; + } + if tx.is_error { + result.num_errors += 1; + } + }, + Ok(None) => break, + Err(_) => { + println!("Still waiting for a result after 10s..."); + if result.num_transactions > 0 { + println!("Results so far:"); + print_summary(&result); + println!(); + } + }, + } + } + result + }); + + // Result emitter + while let Some(transaction_id) = submitted_rx.recv().await { + let mut client = clients[count % clients.len()].clone(); + let results_tx = results_tx.clone(); + bounded_spawn + .spawn(async move { + loop { + match client + .get_transaction_result(GetTransactionResultRequest { transaction_id }) + .await + { + Ok(result) => { + if result.is_finalized { + let result = if let Some(diff) = result.result.unwrap().finalize.result.accept() { + TxFinalized { + is_committed: true, + is_error: false, + num_up_substates: diff.up_len(), + num_down_substates: diff.down_len(), + execution_time: result.execution_time.unwrap(), + } + } else { + TxFinalized { + is_committed: false, + is_error: false, + num_up_substates: 0, + num_down_substates: 0, + execution_time: result.execution_time.unwrap(), + } + }; + + results_tx.send(result).await.unwrap(); + break; + } else { + sleep(Duration::from_secs(1)).await; + } + }, + Err(e) => { + println!("Failed to get transaction result: {}", e); + results_tx + .send(TxFinalized { + is_committed: false, + is_error: true, + num_up_substates: 0, + num_down_substates: 0, + execution_time: Duration::from_secs(0), + }) + .await + .unwrap(); + break; + }, + } + } + }) + .await; + + count += 1; } - Ok(()) + // Drop the remaining sender handle so that the result collector ends when all results have been received + drop(results_tx); + results_handle.await.unwrap() +} + +struct TxFinalized { + pub is_committed: bool, + pub is_error: bool, + pub num_up_substates: usize, + pub num_down_substates: usize, + pub execution_time: Duration, +} + +#[derive(Debug, Clone)] +pub struct StressTestResultSummary { + pub num_transactions: usize, + pub num_committed: usize, + pub num_errors: usize, + pub num_up_substates: usize, + pub num_down_substates: usize, + pub slowest_execution_time: Duration, + pub fastest_execution_time: Duration, + pub total_execution_time: Duration, +} + +impl Default for StressTestResultSummary { + fn default() -> Self { + Self { + num_transactions: 0, + num_committed: 0, + num_errors: 0, + num_up_substates: 0, + num_down_substates: 0, + slowest_execution_time: Duration::from_secs(0), + fastest_execution_time: Duration::MAX, + total_execution_time: Duration::from_secs(0), + } + } +} + +fn print_summary(summary: &StressTestResultSummary) { + println!("Summary:"); + println!( + " Success rate: {:.2}%", + summary.num_committed as f64 / summary.num_transactions as f64 * 100.0 + ); + println!(" Transactions submitted: {}", summary.num_transactions); + println!(" Transactions committed: {}", summary.num_committed); + println!(" Transactions errored: {}", summary.num_errors); + println!(" Up substates: {}", summary.num_up_substates); + println!(" Down substates: {}", summary.num_down_substates); + println!( + " Total execution time: {:.2?} (slowest: {:.2?}, fastest: {:.2?})", + summary.total_execution_time, summary.slowest_execution_time, summary.fastest_execution_time + ); + + println!( + " Avg execution time: {}", + summary + .total_execution_time + .as_millis() + .checked_div(summary.num_committed as u128) + .map(|n| format!("{}ms", n)) + .unwrap_or_else(|| "--".to_string()) + ); }