diff --git a/Cargo.lock b/Cargo.lock index d1f5ac96d1..4233c8a334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "consensus_tests" -version = "0.14.2" +version = "0.15.0" dependencies = [ "fern", "futures 0.3.31", @@ -2287,6 +2287,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "ctr" version = "0.9.2" @@ -2607,7 +2628,7 @@ dependencies = [ [[package]] name = "db_inspector" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -3320,6 +3341,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime 2.2.0", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.8" @@ -3746,12 +3780,15 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generate_ristretto_value_lookup" -version = "0.14.2" +version = "0.15.0" dependencies = [ "clap 3.2.25", + "futures 0.3.31", "human_bytes", "humantime 2.2.0", "tari_crypto", + "tari_ootle_wallet_crypto", + "tokio", ] [[package]] @@ -4993,7 +5030,7 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "integration_tests" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "config", @@ -5119,6 +5156,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -5761,7 +5809,7 @@ dependencies = [ [[package]] name = "libp2p-messaging" -version = "0.14.2" +version = "0.15.0" dependencies = [ "async-trait", "futures-bounded", @@ -5940,7 +5988,7 @@ dependencies = [ [[package]] name = "libp2p-substream" -version = "0.14.2" +version = "0.15.0" dependencies = [ "libp2p", "prometheus-client", @@ -8466,7 +8514,7 @@ dependencies = [ [[package]] name = "proto_builder" -version = "0.14.2" +version = "0.15.0" dependencies = [ "prost-build 0.14.1", "sha2", @@ -8825,9 +8873,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -8835,9 +8883,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -10300,7 +10348,7 @@ dependencies = [ [[package]] name = "sqlite_message_logger" -version = "0.14.2" +version = "0.15.0" dependencies = [ "chrono", "diesel", @@ -10341,7 +10389,7 @@ checksum = "e7386b49cb287f6fafbfd3bd604914bccb99fb8d53483f40e1ecfda5d45f3370" [[package]] name = "state_store_tests" -version = "0.14.2" +version = "0.15.0" dependencies = [ "env_logger 0.11.8", "indexmap 2.11.4", @@ -10730,7 +10778,7 @@ dependencies = [ [[package]] name = "tari_base_node_client" -version = "0.14.2" +version = "0.15.0" dependencies = [ "log", "minotari_app_grpc", @@ -10954,7 +11002,7 @@ dependencies = [ [[package]] name = "tari_consensus" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "indexmap 2.11.4", @@ -10978,7 +11026,7 @@ dependencies = [ [[package]] name = "tari_consensus_types" -version = "0.14.2" +version = "0.15.0" dependencies = [ "borsh", "serde", @@ -11080,7 +11128,7 @@ dependencies = [ [[package]] name = "tari_engine" -version = "0.14.2" +version = "0.15.0" dependencies = [ "blake2", "cargo_toml 0.22.3", @@ -11109,7 +11157,7 @@ dependencies = [ [[package]] name = "tari_engine_types" -version = "0.14.2" +version = "0.15.0" dependencies = [ "base64 0.21.7", "bincode 2.0.1", @@ -11136,7 +11184,7 @@ dependencies = [ [[package]] name = "tari_epoch_manager" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "log", @@ -11158,7 +11206,7 @@ dependencies = [ [[package]] name = "tari_epoch_oracles" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "blake2", @@ -11200,7 +11248,7 @@ dependencies = [ [[package]] name = "tari_indexer" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "async-graphql", @@ -11264,7 +11312,7 @@ dependencies = [ [[package]] name = "tari_indexer_client" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bounded-vec", @@ -11294,7 +11342,7 @@ dependencies = [ [[package]] name = "tari_indexer_lib" -version = "0.14.2" +version = "0.15.0" dependencies = [ "log", "serde", @@ -11370,7 +11418,7 @@ dependencies = [ [[package]] name = "tari_networking" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -11407,7 +11455,7 @@ dependencies = [ [[package]] name = "tari_ootle_address" -version = "0.14.2" +version = "0.15.0" dependencies = [ "bech32", "bincode 2.0.1", @@ -11423,7 +11471,7 @@ dependencies = [ [[package]] name = "tari_ootle_app_utilities" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11462,7 +11510,7 @@ dependencies = [ [[package]] name = "tari_ootle_common_types" -version = "0.14.2" +version = "0.15.0" dependencies = [ "blake2", "borsh", @@ -11490,7 +11538,7 @@ dependencies = [ [[package]] name = "tari_ootle_p2p" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -11512,7 +11560,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bitflags 2.9.2", @@ -11537,7 +11585,7 @@ dependencies = [ [[package]] name = "tari_ootle_storage_sqlite" -version = "0.14.2" +version = "0.15.0" dependencies = [ "diesel", "diesel_migrations", @@ -11557,7 +11605,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_cli" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -11586,7 +11634,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_crypto" -version = "0.14.2" +version = "0.15.0" dependencies = [ "argon2 0.5.3", "blake2", @@ -11610,7 +11658,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "blake2", @@ -11645,7 +11693,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_sdk_services" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11670,7 +11718,7 @@ dependencies = [ [[package]] name = "tari_ootle_wallet_storage_sqlite" -version = "0.14.2" +version = "0.15.0" dependencies = [ "bigdecimal", "diesel", @@ -11694,7 +11742,7 @@ dependencies = [ [[package]] name = "tari_ootle_walletd" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -11779,7 +11827,7 @@ dependencies = [ [[package]] name = "tari_rpc_framework" -version = "0.14.2" +version = "0.15.0" dependencies = [ "async-trait", "bitflags 2.9.2", @@ -11803,7 +11851,7 @@ dependencies = [ [[package]] name = "tari_rpc_macros" -version = "0.14.2" +version = "0.15.0" dependencies = [ "proc-macro2", "quote", @@ -11812,7 +11860,7 @@ dependencies = [ [[package]] name = "tari_rpc_state_sync" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "futures 0.3.31", @@ -11832,7 +11880,7 @@ dependencies = [ [[package]] name = "tari_scaffolder" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -11904,7 +11952,7 @@ dependencies = [ [[package]] name = "tari_signaling_server" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -11930,7 +11978,7 @@ dependencies = [ [[package]] name = "tari_state_store_rocksdb" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -11956,7 +12004,7 @@ dependencies = [ [[package]] name = "tari_state_tree" -version = "0.14.2" +version = "0.15.0" dependencies = [ "indexmap 2.11.4", "log", @@ -11983,7 +12031,7 @@ dependencies = [ [[package]] name = "tari_swarm" -version = "0.14.2" +version = "0.15.0" dependencies = [ "libp2p", "libp2p-messaging", @@ -11993,7 +12041,7 @@ dependencies = [ [[package]] name = "tari_swarm_daemon" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "async-trait", @@ -12049,7 +12097,7 @@ dependencies = [ [[package]] name = "tari_template_builtin" -version = "0.14.2" +version = "0.15.0" dependencies = [ "tari_engine_types", "tari_template_lib", @@ -12101,7 +12149,7 @@ dependencies = [ [[package]] name = "tari_template_manager" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bytes 1.10.1", @@ -12167,7 +12215,7 @@ dependencies = [ [[package]] name = "tari_transaction" -version = "0.14.2" +version = "0.15.0" dependencies = [ "borsh", "hex", @@ -12255,7 +12303,7 @@ dependencies = [ [[package]] name = "tari_transaction_manifest" -version = "0.14.2" +version = "0.15.0" dependencies = [ "proc-macro2", "serde_json", @@ -12289,7 +12337,7 @@ dependencies = [ [[package]] name = "tari_validator_node" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "axum 0.8.4", @@ -12351,7 +12399,7 @@ dependencies = [ [[package]] name = "tari_validator_node_cli" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12379,7 +12427,7 @@ dependencies = [ [[package]] name = "tari_validator_node_client" -version = "0.14.2" +version = "0.15.0" dependencies = [ "indexmap 2.11.4", "multiaddr 0.18.1", @@ -12402,7 +12450,7 @@ dependencies = [ [[package]] name = "tari_validator_node_rpc" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "prost 0.14.1", @@ -12424,7 +12472,7 @@ dependencies = [ [[package]] name = "tari_wallet_daemon_client" -version = "0.14.2" +version = "0.15.0" dependencies = [ "reqwest 0.11.27", "serde", @@ -12445,7 +12493,7 @@ dependencies = [ [[package]] name = "tari_watcher" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "clap 3.2.25", @@ -12473,7 +12521,7 @@ dependencies = [ [[package]] name = "tariswap_bench" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "clap 4.5.48", @@ -13130,9 +13178,31 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "traffic-sim" +version = "0.15.0" +dependencies = [ + "anyhow", + "clap 4.5.48", + "csv", + "env_logger 0.10.2", + "log", + "rand 0.8.5", + "reqwest 0.11.27", + "serde", + "serde_json", + "tari_indexer_client", + "tari_ootle_common_types", + "tari_ootle_wallet_sdk", + "tari_template_lib", + "tari_transaction", + "tari_wallet_daemon_client", + "tokio", +] + [[package]] name = "transaction_generator" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "bincode 2.0.1", @@ -13152,7 +13222,7 @@ dependencies = [ [[package]] name = "transaction_submitter" -version = "0.14.2" +version = "0.15.0" dependencies = [ "anyhow", "clap 4.5.48", diff --git a/Cargo.toml b/Cargo.toml index a5fd20e499..1caf4f6666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ # NOTE: When editing this version, also edit the versions in template_built_in/templates/account and account_nft [workspace.package] -version = "0.14.2" +version = "0.15.0" edition = "2021" authors = ["The Tari Development Community"] repository = "https://github.com/tari-project/tari-ootle" @@ -64,7 +64,7 @@ members = [ "utilities/transaction_submitter", "utilities/transaction_submitter", "utilities/generate_ristretto_value_lookup", - "utilities/db_inspector", + "utilities/db_inspector", "utilities/traffic-sim", ] resolver = "2" diff --git a/applications/tari_indexer/src/rest_api/handlers/utxos.rs b/applications/tari_indexer/src/rest_api/handlers/utxos.rs index 845b8e9e4c..b6a9b87390 100644 --- a/applications/tari_indexer/src/rest_api/handlers/utxos.rs +++ b/applications/tari_indexer/src/rest_api/handlers/utxos.rs @@ -4,13 +4,20 @@ use std::collections::HashSet; use axum::{ + extract::Query, http::header::HeaderMap, response::{IntoResponse, Response}, Extension, Json, }; use log::*; -use tari_indexer_client::types::{GetUnspentUtxosRequest, GetUnspentUtxosResponse, GetUtxoUpdatesRequest}; +use tari_indexer_client::types::{ + GetUtxoUpdatesRequest, + GetUtxosRequest, + GetUtxosResponse, + ListUtxosRequest, + ListUtxosResponse, +}; use tari_ootle_common_types::NumPreshards; use crate::rest_api::{ @@ -92,8 +99,8 @@ pub async fn stream_utxo_updates( )] pub async fn fetch_utxos( Extension(context): Extension, - Json(req): Json, -) -> HandlerResult> { + Json(req): Json, +) -> HandlerResult> { if req.tag_and_nonce_pairs.len() > 1000 { return Err(ErrorResponse::bad_request("cannot query more than 1000 UTXOs")); } @@ -102,5 +109,24 @@ pub async fn fetch_utxos( .get_unspent_utxos(&req.resource_address, &req.tag_and_nonce_pairs) .map_err(ErrorResponse::anyhow)?; - Ok(Json(GetUnspentUtxosResponse { utxos })) + Ok(Json(GetUtxosResponse { utxos })) +} + +#[utoipa::path(get, path = "/utxos", description = "List full UTXO data")] +pub async fn list_utxos( + Extension(context): Extension, + Query(req): Query, +) -> HandlerResult> { + if req.limit == 0 { + return Err(ErrorResponse::bad_request("limit must be greater than 0")); + } + if req.limit > 1000 { + return Err(ErrorResponse::bad_request("cannot query more than 1000 UTXOs")); + } + let utxos = context + .substate_manager() + .list_utxos(&req.resource_address, req.from_id, req.limit) + .map_err(ErrorResponse::anyhow)?; + + Ok(Json(ListUtxosResponse { utxos })) } diff --git a/applications/tari_indexer/src/rest_api/server.rs b/applications/tari_indexer/src/rest_api/server.rs index 061c5f001a..cd22f0fec7 100644 --- a/applications/tari_indexer/src/rest_api/server.rs +++ b/applications/tari_indexer/src/rest_api/server.rs @@ -42,6 +42,7 @@ const REQUEST_BODY_LIMIT: usize = 4 * 1024 * 1024; // 4 MB handlers::templates::get_template_definition, handlers::templates::list_templates, handlers::utxos::fetch_utxos, + handlers::utxos::list_utxos, handlers::utxos::stream_utxo_updates, handlers::transaction_receipts::list_transaction_receipts, handlers::transaction_receipts::get_transaction_receipt @@ -92,6 +93,7 @@ impl Server { ) .route("/non-fungibles", get(handlers::nfts::get_non_fungibles)) // Placeholder for future non-fungible endpoints .nest("/utxos", Router::new() + .route("/", get(handlers::utxos::list_utxos)) .route("/fetch", post(handlers::utxos::fetch_utxos)) .route("/stream", post(handlers::utxos::stream_utxo_updates)) ) diff --git a/applications/tari_indexer/src/storage_sqlite/reader.rs b/applications/tari_indexer/src/storage_sqlite/reader.rs index aa55a4db98..71952a8e01 100644 --- a/applications/tari_indexer/src/storage_sqlite/reader.rs +++ b/applications/tari_indexer/src/storage_sqlite/reader.rs @@ -414,7 +414,7 @@ impl IndexerStoreReadTransaction for SqliteStoreReadTransaction<'_> { let tr = alias!(transaction_receipts as tr); let subquery = tr .select(tr.field(transaction_receipts::id)) - .filter(transaction_receipts::address.eq(last_id.to_string())) + .filter(tr.field(transaction_receipts::address).eq(last_id.to_string())) .limit(1) .single_value() .assume_not_null(); @@ -560,6 +560,52 @@ impl IndexerStoreReadTransaction for SqliteStoreReadTransaction<'_> { Ok((max_state_version, updates)) } + fn utxos_list( + &mut self, + resource_address: &ResourceAddress, + from_id: Option, + limit: u32, + ) -> Result, StorageError> { + const OPERATION: &str = "utxos_list"; + use crate::storage_sqlite::schema::utxos; + + let mut query = utxos::table + .filter(utxos::resource_address.eq(resource_address.to_string())) + .filter(utxos::is_spent.eq(false)) + .filter(utxos::is_burnt.eq(false)) + .into_boxed(); + + if let Some(from_id) = from_id { + let uxo = alias!(utxos as uxo); + let subquery = uxo + .select(uxo.field(utxos::id)) + .filter(uxo.field(utxos::commitment).eq(from_id.to_commitment_hex_string())) + .limit(1) + .single_value() + .assume_not_null(); + query = query.filter(utxos::id.gt(subquery)); + } + + let rows = query + .limit(i64::from(limit)) + .order_by(utxos::id.asc()) + .load_iter::(self.connection()) + .map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + })?; + + rows.map(|res| { + res.map_err(|e| StorageError::QueryError { + reason: format!("{OPERATION}: {}", e), + }) + .and_then(|row| { + let (address, utxo) = row.try_convert_to_utxo()?; + Ok((*address.id(), utxo)) + }) + }) + .collect() + } + fn utxos_get_unspent_by_public_nonce_and_tag( &mut self, resource_address: &ResourceAddress, diff --git a/applications/tari_indexer/src/store.rs b/applications/tari_indexer/src/store.rs index a2885855d5..5c5999615e 100644 --- a/applications/tari_indexer/src/store.rs +++ b/applications/tari_indexer/src/store.rs @@ -150,6 +150,13 @@ pub trait IndexerStoreReadTransaction { limit: u32, ) -> Result<(StateVersion, Vec), StorageError>; + fn utxos_list( + &mut self, + resource_address: &ResourceAddress, + from_id: Option, + limit: u32, + ) -> Result, StorageError>; + fn utxos_get_unspent_by_public_nonce_and_tag( &mut self, resource_address: &ResourceAddress, diff --git a/applications/tari_indexer/src/substate_manager.rs b/applications/tari_indexer/src/substate_manager.rs index 154133bbe5..238c65495d 100644 --- a/applications/tari_indexer/src/substate_manager.rs +++ b/applications/tari_indexer/src/substate_manager.rs @@ -140,6 +140,18 @@ impl SubstateManager { Ok(utxos) } + pub fn list_utxos( + &self, + resource_address: &ResourceAddress, + from_id: Option, + limit: u32, + ) -> Result, anyhow::Error> { + let utxos = self + .substate_store + .with_read_tx(|tx| tx.utxos_list(resource_address, from_id, limit))?; + Ok(utxos) + } + pub async fn get_substate( &self, substate_id: &SubstateId, diff --git a/applications/tari_validator_node/src/cli.rs b/applications/tari_validator_node/src/cli.rs index 0fb86a3996..17f790296b 100644 --- a/applications/tari_validator_node/src/cli.rs +++ b/applications/tari_validator_node/src/cli.rs @@ -24,8 +24,14 @@ use std::{net::SocketAddr, path::PathBuf}; use clap::Parser; use minotari_app_utilities::common_cli_args::CommonCliArgs; -use tari_common::configuration::{ConfigOverrideProvider, Network as L1Network}; -use tari_ootle_app_utilities::{configuration::convert_l1_network_to_network, p2p_config::ReachabilityMode}; +use tari_common::{ + configuration::{ConfigOverrideProvider, Network as L1Network}, + ConfigPath, +}; +use tari_ootle_app_utilities::{ + configuration::convert_l1_network_to_network, + p2p_config::{PeerSeedsConfig, ReachabilityMode}, +}; use tari_ootle_common_types::Network; use url::Url; @@ -100,7 +106,14 @@ impl ConfigOverrideProvider for Cli { )); } if !self.peer_seeds.is_empty() { - overrides.push(("p2p.seeds.peer_seeds".to_string(), self.peer_seeds.join(","))); + overrides.push(( + format!("{}.peer_seeds", PeerSeedsConfig::main_key_prefix()), + self.peer_seeds.join(","), + )); + overrides.push(( + format!("{}.{}.peer_seeds", network, PeerSeedsConfig::main_key_prefix()), + self.peer_seeds.join(","), + )); } if let Some(listener_port) = self.listener_port { overrides.push(( diff --git a/applications/tari_wallet_cli/src/command/transaction.rs b/applications/tari_wallet_cli/src/command/transaction.rs index c759017167..6ea26378c5 100644 --- a/applications/tari_wallet_cli/src/command/transaction.rs +++ b/applications/tari_wallet_cli/src/command/transaction.rs @@ -41,7 +41,11 @@ use tari_engine_types::{ }; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{Epoch, SubstateAddress, SubstateRequirement}; -use tari_ootle_wallet_sdk::{apis::confidential_transfer::ConfidentialTransferInputSelection, crypto::memo::Memo}; +use tari_ootle_wallet_sdk::{ + apis::confidential_transfer::UtxoInputSelection, + crypto::memo::Memo, + models::BranchAndKeyId, +}; use tari_template_lib::{ constants::STEALTH_TARI_RESOURCE_ADDRESS, models::{BucketId, NonFungibleAddress, NonFungibleId}, @@ -246,6 +250,10 @@ pub async fn handle_submit(args: SubmitArgs, client: &mut WalletDaemonClient) -> fee_account = client.accounts_get_default().await?.account; } + let owner_key_id = fee_account + .owner_key_id + .ok_or_else(|| anyhow!("Fee account does not have an owner key ID"))?; + let SettingsGetResponse { network, .. } = client.get_settings().await?; let mut builder = Transaction::builder() @@ -275,20 +283,22 @@ pub async fn handle_submit(args: SubmitArgs, client: &mut WalletDaemonClient) -> let resp = client .submit_transaction_dry_run(TransactionSubmitDryRunRequest { transaction, - signing_key_id: None, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: common.detect_inputs.unwrap_or(true), detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }) .await?; wait_transaction_result(resp.transaction_id, client).await?; } else { let request = TransactionSubmitRequest { transaction, - signing_key_id: None, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: common.detect_inputs.unwrap_or(true), detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = client.submit_transaction(&request).await?; wait_transaction_result(resp.transaction_id, client).await?; @@ -312,6 +322,10 @@ async fn handle_submit_manifest( fee_account = client.accounts_get_default().await?.account; } + let owner_key_id = fee_account + .owner_key_id + .ok_or_else(|| anyhow!("Fee account does not have an owner key ID"))?; + let SettingsGetResponse { network, .. } = client.get_settings().await?; let builder = Transaction::builder() @@ -338,20 +352,22 @@ async fn handle_submit_manifest( let resp = client .submit_transaction_dry_run(TransactionSubmitDryRunRequest { transaction, - signing_key_id: fee_account.owner_key_id, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: common.detect_inputs.unwrap_or(true), detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }) .await?; summarize(&resp.result.finalize, timer.elapsed()); } else { let request = TransactionSubmitRequest { transaction, - signing_key_id: fee_account.owner_key_id, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: common.detect_inputs.unwrap_or(true), detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = client.submit_transaction(&request).await?; @@ -426,7 +442,7 @@ pub async fn handle_confidential_transfer( let resp = client .accounts_confidential_transfer(ConfidentialTransferRequest { account: source_account, - input_selection: ConfidentialTransferInputSelection::PreferConfidential, + input_selection: UtxoInputSelection::PreferConfidential, amount: amount.into(), resource_address: resource_address.unwrap_or(STEALTH_TARI_RESOURCE_ADDRESS), destination_address, diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index 92bb2972a4..a22c9872f1 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -1,9 +1,9 @@ // Copyright 2023 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use std::{array, collections::HashSet}; +use std::{collections::HashSet, iter}; -use anyhow::Context; +use anyhow::{anyhow, Context}; use axum_extra::headers::authorization::Bearer; use indexmap::IndexMap; use log::*; @@ -26,10 +26,11 @@ use tari_ootle_wallet_crypto::{ use tari_ootle_wallet_sdk::{ apis::{ confidential_transfer::ConfidentialTransferParams, + stealth_outputs::TransferStatementParams, stealth_transfer::{StealthTransferParams, TransferOutput}, substate::ValidatorScanResult, }, - models::{KeyBranch, NewAccountData}, + models::{BranchAndKeyId, KeyBranch, KeyId, NewAccountData, WalletLockDropGuard}, }; use tari_ootle_wallet_sdk_services::events::TransactionSubmittedEvent; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; @@ -56,6 +57,8 @@ use tari_wallet_daemon_client::{ AccountsCreateOrGetResponse, AccountsCreateRequest, AccountsCreateResponse, + AccountsCreateStealthTransferStatementRequest, + AccountsCreateStealthTransferStatementResponse, AccountsGetBalancesRequest, AccountsGetBalancesResponse, AccountsListRequest, @@ -361,7 +364,14 @@ pub async fn handle_get( ) -> Result { context.check_auth(token, &[JrpcPermission::AccountInfo])?; let sdk = context.wallet_sdk(); - let account = get_account(&req.name_or_address, &sdk.accounts_api())?; + let account = get_account(&req.name_or_address, &sdk.accounts_api()) + .optional()? + .ok_or_else(|| { + not_found(format!( + "Account with name or address '{}' not found", + req.name_or_address + )) + })?; Ok(AccountGetResponse { account: account.account, address: account.address, @@ -538,9 +548,9 @@ pub async fn handle_claim_burn( let public_signer_key = sdk.key_manager_api().next_public_key(KeyBranch::Nonce)?; let pay_fee_and_mint_output = sdk.stealth_crypto_api().generate_transfer_statement( - array::from_ref(&input), + iter::once(&input), 0, - array::from_ref(&output_statement), + iter::once(&output_statement), max_fee, public_signer_key.public_key.to_byte_type(), )?; @@ -969,6 +979,7 @@ pub async fn handle_stealth_transfer( }; let params = StealthTransferParams { + fee_input_selection: req.fee_input_selection, input_selection: req.input_selection, resource_address: req.resource_address, max_fee: req.max_fee, @@ -1054,6 +1065,131 @@ pub async fn handle_stealth_transfer( }) .await? } + +#[allow(clippy::too_many_lines)] +pub async fn handle_create_stealth_transfer_statement( + context: &HandlerContext, + token: Option<&Bearer>, + req: AccountsCreateStealthTransferStatementRequest, +) -> Result { + context.check_auth(token, &[JrpcPermission::TransactionSend(None)])?; + let sdk = context.wallet_sdk().clone(); + if req.requests.is_empty() { + return Err(invalid_params( + "requests", + Some("at least one transfer request must be provided"), + )); + } + + if req.requests.len() > 16 { + return Err(invalid_params( + "requests", + Some("a maximum of 16 transfer requests can be processed at once"), + )); + } + + let mut required_signers = HashSet::new(); + let lock_id = sdk.stealth_outputs_api().create_lock()?; + let lock_guard = WalletLockDropGuard::new(lock_id, sdk.store().clone()); + let mut statements = Vec::with_capacity(req.requests.len()); + for req in req.requests { + let sender_account = get_account(&req.sender_account, &sdk.accounts_api())?; + let Some(sender_key_id) = sender_account.owner_key_id() else { + return Err(invalid_params( + "owner_account", + Some("cannot transfer from an account without an owner key"), + )); + }; + + let resource = sdk.substate_api().fetch_resource(req.resource_address).await?; + + if !resource.resource_type().is_stealth() { + return Err(invalid_params( + "resource_address", + Some(format!( + "Resource is not a stealth resource (type: {})", + resource.resource_type() + )), + )); + } + + let amount_to_spend = req.total_output_amount(); + + let inputs = req + .input_selection + .as_selection() + .map(|sel| { + sdk.stealth_transfer_api().lock_inputs_for_transfer( + lock_id, + sender_account.component_address(), + req.resource_address, + amount_to_spend, + sel, + ) + }) + .transpose()?; + + let must_sign_with_account_key = inputs.as_ref().is_some_and(|i| i.revealed.is_positive()); + let (signing_key_branch, signing_key_id) = if must_sign_with_account_key { + (KeyBranch::Account, sender_key_id) + } else { + let next_index = sdk.key_manager_api().next_derived_key_index(KeyBranch::Nonce)?; + (KeyBranch::Nonce, KeyId::derived(next_index)) + }; + + let required_signer = sdk + .key_manager_api() + .get_public_key(signing_key_branch, signing_key_id)?; + let required_signer = required_signer.public_key.to_byte_type(); + + let outputs = req + .outputs + .iter() + .filter(|o| o.blinded_amount.is_positive()) + .map(TryInto::try_into) + .collect::, _>>()?; + + let statement = sdk + .stealth_outputs_api() + .generate_transfer_statement(TransferStatementParams { + spend_key_branch: KeyBranch::Account, + spend_key_id: sender_key_id, + view_only_key_id: sender_account.view_only_key_id(), + resource_address: &req.resource_address, + resource_view_key: resource + .to_view_key_public_key() + .map_err(|e| anyhow!("Failed to decode resource public view key: {e}"))?, + inputs: inputs.as_ref().map(|i| i.inputs.as_slice()).unwrap_or(&[]), + input_revealed_amount: req + .input_selection + .as_from_bucket() + .unwrap_or(Amount::zero()) + .checked_add_positive(inputs.as_ref().map(|i| i.revealed).unwrap_or(Amount::zero())) + .ok_or_else(|| { + invalid_params( + "input_revealed_amount", + Some("input revealed amount overflowed or was negative"), + ) + })?, + outputs, + output_revealed_amount: req.outputs.iter().map(|o| o.revealed_amount).sum(), + required_signer, + })?; + + required_signers.insert(BranchAndKeyId::new(signing_key_branch, signing_key_id)); + statements.push(statement); + } + + // Return without unlocking the outputs + lock_guard.disarm(); + + Ok(AccountsCreateStealthTransferStatementResponse { + statements, + lock_id, + signing_keys: required_signers.into_iter().collect(), + }) +} + pub async fn handle_associate_stealth_resource( context: &HandlerContext, token: Option<&Bearer>, diff --git a/applications/tari_walletd/src/handlers/helpers.rs b/applications/tari_walletd/src/handlers/helpers.rs index 0ee7a49541..31ae89e9b0 100644 --- a/applications/tari_walletd/src/handlers/helpers.rs +++ b/applications/tari_walletd/src/handlers/helpers.rs @@ -147,7 +147,9 @@ where { let result; if let Some(a) = account { - result = get_account(a, accounts_api)?; + result = get_account(a, accounts_api) + .optional()? + .ok_or_else(|| not_found(format!("Account '{a}' not found.",)))?; } else { result = accounts_api .get_default() diff --git a/applications/tari_walletd/src/handlers/stealth_utxos.rs b/applications/tari_walletd/src/handlers/stealth_utxos.rs index 10280938f0..8496f78d69 100644 --- a/applications/tari_walletd/src/handlers/stealth_utxos.rs +++ b/applications/tari_walletd/src/handlers/stealth_utxos.rs @@ -6,7 +6,7 @@ use std::fs; use anyhow::anyhow; use axum_extra::headers::authorization::Bearer; use indexmap::IndexMap; -use log::info; +use log::{info, warn}; use tari_ootle_wallet_crypto::{AlwaysMissLookupTable, IoReaderValueLookup}; use tari_template_lib::models::UtxoAddress; use tari_wallet_daemon_client::{ @@ -19,11 +19,14 @@ use tari_wallet_daemon_client::{ UtxoInfo, }, }; -use tokio::{task::spawn_blocking, time::Instant}; +use tokio::{ + task::{spawn_blocking, AbortHandle}, + time::Instant, +}; use crate::handlers::{helpers::invalid_params, HandlerContext}; -const LOG_TARGET: &str = "tari::walletd::handlers::stealth_utxos"; +const LOG_TARGET: &str = "tari::ootle::walletd::handlers::stealth_utxos"; pub async fn handle_list( context: &HandlerContext, @@ -54,6 +57,7 @@ pub async fn handle_list( }) } +#[allow(clippy::too_many_lines)] pub async fn handle_decrypt_value( context: &HandlerContext, token: Option<&Bearer>, @@ -61,6 +65,9 @@ pub async fn handle_decrypt_value( ) -> Result { let sdk = context.wallet_sdk(); context.check_auth(token, &[JrpcPermission::Admin])?; + if req.ids.is_empty() { + return Err(invalid_params("ids", Some("At least one UTXO ID must be provided"))); + } if req.ids.len() > 10 { return Err(invalid_params( @@ -81,7 +88,7 @@ pub async fn handle_decrypt_value( // Get view secret key let view_key = sdk.key_manager_api().get_elgamal_encrypted_view_key(req.view_key_id)?; - let value_range = req.minimum_expected_value.unwrap_or(0)..=req.maximum_expected_value.unwrap_or(10_000_000_000); + let value_range = req.minimum_expected_value.unwrap_or(0)..=req.maximum_expected_value; // NOTE: we iterate in a random order (HashMap) but collect into a deterministic order (IndexMap) so that the // results are always in the same order for the same input @@ -101,40 +108,81 @@ pub async fn handle_decrypt_value( let timer = Instant::now(); let elgamal_proofs = proofs.values().copied().cloned().collect::>(); let sdk = sdk.clone(); - let balances = match context.config().value_lookup_table_file.clone() { - Some(file) => { - spawn_blocking(move || { - let mut file = fs::File::open(&file) - .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", file.display()))?; - let mut lookup = IoReaderValueLookup::load(&mut file)?; - - let balance = sdk.viewable_balance_api().try_brute_force_commitment_balances( - &view_key.key, - elgamal_proofs.iter(), - value_range, - &mut lookup, - )?; - - anyhow::Ok(balance) - }) - .await?? - }, + let handle = match context.config().value_lookup_table_file.clone() { + Some(path) => spawn_blocking(move || { + let mut file = fs::File::open(&path) + .map_err(|e| anyhow!("Unable to load value lookup file '{}': {e}", path.display()))?; + let mut lookup = IoReaderValueLookup::load(&mut file)?; + info!( + target: LOG_TARGET, + "Using value lookup table from file '{}' ({}-{}) for brute force balance lookup", + path.display(), + lookup.range().start(), + lookup.range().end() + ); + + let start = value_range.start(); + let end = value_range.end(); + if start < lookup.range().start() || end > lookup.range().end() { + warn!( + target: LOG_TARGET, + "The requested value range ({}-{}) is outside the loaded value lookup table range ({}-{}). \ + This query may take excessive amount of time.", + start, + end, + lookup.range().start(), + lookup.range().end() + ); + } + + let balance = sdk.viewable_balance_api().try_brute_force_commitment_balances( + &view_key.key, + elgamal_proofs.iter(), + value_range, + &mut lookup, + )?; + + anyhow::Ok(balance) + }), None => { + warn!( + target: LOG_TARGET, + "No value lookup table file configured. Generating a temporary lookup table that always misses. \ + This will make the brute force balance lookup very slow for high-value outputs." + ); spawn_blocking(move || { - sdk.viewable_balance_api().try_brute_force_commitment_balances( + let balances = sdk.viewable_balance_api().try_brute_force_commitment_balances( &view_key.key, elgamal_proofs.iter(), value_range, &mut AlwaysMissLookupTable, - ) + )?; + anyhow::Ok(balances) }) - .await?? }, }; + struct AbortOnDropGuard { + handle: AbortHandle, + } + + impl Drop for AbortOnDropGuard { + fn drop(&mut self) { + if !self.handle.is_finished() { + info!(target: LOG_TARGET, "Aborting brute force balance lookup task"); + self.handle.abort(); + } + } + } + + // If this request is abandoned, abort the blocking task + let _drop_guard = AbortOnDropGuard { + handle: handle.abort_handle(), + }; + let balances = handle.await??; info!(target: LOG_TARGET, "Brute force balance lookup took {:.2?}", timer.elapsed()); Ok(StealthUtxosDecryptValueResponse { - balances: proofs.into_keys().zip(balances).collect(), + values: proofs.into_keys().zip(balances).collect(), }) } diff --git a/applications/tari_walletd/src/handlers/transaction.rs b/applications/tari_walletd/src/handlers/transaction.rs index c52dbdfe4f..a66079836b 100644 --- a/applications/tari_walletd/src/handlers/transaction.rs +++ b/applications/tari_walletd/src/handlers/transaction.rs @@ -11,7 +11,7 @@ use tari_engine_types::ToByteType; use tari_ootle_common_types::{optional::Optional, Epoch, Network}; use tari_ootle_wallet_sdk::{ apis::{config::ConfigKey, transaction::TransactionApiError}, - models::KeyBranch, + models::{BranchAndKeyId, KeyBranch}, network::WalletQueryErrorStatus, }; use tari_ootle_wallet_sdk_services::{events::WalletEvent, transaction_service::TransactionServiceError}; @@ -78,6 +78,12 @@ pub async fn handle_submit_instruction( ); } let fee_account = get_account(&req.fee_account, &sdk.accounts_api())?; + let owner_key_id = fee_account.owner_key_id().ok_or_else(|| { + invalid_params( + "fee_account", + Some("Fee account does not have an owner key set".to_string()), + ) + })?; let transaction = builder .fee_transaction_pay_from_component(*fee_account.component_address(), req.max_fee) @@ -87,10 +93,11 @@ pub async fn handle_submit_instruction( let request = TransactionSubmitRequest { transaction, - signing_key_id: fee_account.owner_key_id(), + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: req.override_inputs.unwrap_or_default(), detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; handle_submit(context, token, request).await } @@ -103,10 +110,6 @@ pub async fn handle_submit( // TODO: fine-grained checks of individual addresses involved (resources, components, etc) context.check_auth(token, &[JrpcPermission::TransactionSend(None)])?; let sdk = context.wallet_sdk(); - let key_api = sdk.key_manager_api(); - // Fetch the key to sign the transaction - let signing_key = key_api.get_key_or_active(KeyBranch::Account, req.signing_key_id)?; - let detected_inputs = if req.detect_inputs { // If we are not overriding inputs, we will use inputs that we know about in the local substate id db let substates = req.transaction.to_referenced_substates()?; @@ -145,26 +148,27 @@ pub async fn handle_submit( req.detect_inputs_use_unversioned, ); - let transaction = context + let mut builder = context .transaction_builder() .with_unsigned_transaction(req.transaction) - .with_inputs(detected_inputs) - .build(); - let transaction = sdk - .local_signer_api() - .sign(KeyBranch::Account, signing_key.key_id, transaction)?; + .with_inputs(detected_inputs); - if log_enabled!(log::Level::Debug) { - for input in transaction.inputs() { - debug!(target: LOG_TARGET, "Input: {}", input) - } + let main_signer = sdk + .key_manager_api() + .get_public_key(req.seal_signer.branch, req.seal_signer.key_id)?; + let main_signer_pk = main_signer.public_key.to_byte_type(); + let mut local_signer = sdk.local_signer_api(); + for key in &req.other_signers { + builder = local_signer.sign_with_context(key.branch, key.key_id, &main_signer_pk, builder)?; } + let transaction = local_signer.sign(req.seal_signer.branch, req.seal_signer.key_id, builder.build())?; + let tx_id = transaction.calculate_id(); - for proof_id in req.proof_ids { - // update the proofs table with the corresponding transaction hash - sdk.confidential_outputs_api() - .locks_set_transaction_id(proof_id, tx_id)?; + for lock_id in req.lock_ids { + // update the locks with the corresponding transaction ID so that they can be finalized/released once the + // transaction resolves + sdk.transaction_api().locks_set_transaction_id(lock_id, tx_id)?; } info!( @@ -215,7 +219,7 @@ pub async fn handle_submit_dry_run( let sdk = context.wallet_sdk(); let key_api = sdk.key_manager_api(); // Fetch the key to sign the transaction - let key = key_api.get_key_or_active(KeyBranch::Account, req.signing_key_id)?; + let key = key_api.get_public_key(req.seal_signer.branch, req.seal_signer.key_id)?; let detected_inputs = if req.detect_inputs { // If we are not overriding inputs, we will use inputs that we know about in the local substate id db @@ -249,10 +253,10 @@ pub async fn handle_submit_dry_run( .local_signer_api() .sign(KeyBranch::Account, key.key_id, transaction)?; - for proof_id in req.proof_ids { + for lock_id in req.lock_ids { // update the proofs table with the corresponding transaction hash - sdk.confidential_outputs_api() - .locks_set_transaction_id(proof_id, transaction.calculate_id())?; + sdk.transaction_api() + .locks_set_transaction_id(lock_id, transaction.calculate_id())?; } info!( @@ -523,6 +527,12 @@ pub async fn handle_publish_template( let sdk = context.wallet_sdk(); let fee_account = get_account_or_default(req.fee_account.as_ref(), &sdk.accounts_api())?; + let owner_key_id = fee_account.owner_key_id().ok_or_else(|| { + invalid_params( + "fee_account", + Some("Fee account does not have an owner key set".to_string()), + ) + })?; // trying to optimize WASM binary let wasm_binary = match optimize_wasm_template(req.binary.as_slice()).await { @@ -545,10 +555,11 @@ pub async fn handle_publish_template( if req.dry_run { let request = TransactionSubmitDryRunRequest { transaction, - signing_key_id: fee_account.owner_key_id(), + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: req.detect_inputs, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = handle_submit_dry_run(context, token, request).await?; if let Some(reject) = resp.result.finalize.any_reject() { @@ -566,10 +577,11 @@ pub async fn handle_publish_template( } let request = TransactionSubmitRequest { transaction, - signing_key_id: fee_account.owner_key_id(), + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: req.detect_inputs, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = handle_submit(context, token, request).await?; Ok(PublishTemplateResponse { diff --git a/applications/tari_walletd/src/jrpc_server.rs b/applications/tari_walletd/src/jrpc_server.rs index 2d14d19a94..f645597c42 100644 --- a/applications/tari_walletd/src/jrpc_server.rs +++ b/applications/tari_walletd/src/jrpc_server.rs @@ -141,6 +141,15 @@ async fn handler( call_handler(context, value, token, accounts::handle_associate_stealth_resource).await }, "stealth_transfer" => call_handler(context, value, token, accounts::handle_stealth_transfer).await, + "create_stealth_transfer_statement" => { + call_handler( + context, + value, + token, + accounts::handle_create_stealth_transfer_statement, + ) + .await + }, "set_default" => call_handler(context, value, token, accounts::handle_set_default).await, "rename" => call_handler(context, value, token, accounts::handle_rename).await, "create_free_test_coins" => { diff --git a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx index a06e0bd113..bb44f811b5 100644 --- a/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx +++ b/applications/tari_walletd/web_ui/src/routes/AssetVault/Tokens/components/SendMoney.tsx @@ -29,7 +29,7 @@ import { SelectChangeEvent } from "@mui/material/Select/Select"; import { BadgeUsage, BalanceEntry, - ConfidentialTransferInputSelection, + UtxoInputSelection, rejectReasonToString, ResourceAddress, ResourceType, @@ -196,7 +196,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { destination_address: transferFormState.address, resourceType: props.resource_type, output_to_revealed: !transferFormState.outputToConfidential, - input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection, + input_selection: transferFormState.inputSelection as UtxoInputSelection, badge_usage: transferFormState.badge ? { Resource: transferFormState.badge } : ("None" as BadgeUsage), output_memo: transferFormState.memo ? { Message: transferFormState.memo } : undefined, }; @@ -271,7 +271,7 @@ export function SendMoneyDialog(props: SendMoneyDialogProps) { destination_address: transferFormState.address, resourceType: props.resource_type, output_to_revealed: !transferFormState.outputToConfidential, - input_selection: transferFormState.inputSelection as ConfidentialTransferInputSelection, + input_selection: transferFormState.inputSelection as UtxoInputSelection, // TODO: support for other types of BadgeUsage badge_usage: transferFormState.badge ? { Resource: transferFormState.badge } : ("None" as BadgeUsage), output_memo: transferFormState.memo ? { Message: transferFormState.memo } : undefined, diff --git a/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx b/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx index 78e0b5159c..4ef860cc16 100644 --- a/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx +++ b/applications/tari_walletd/web_ui/src/routes/FlowEditor/FlowEditor.tsx @@ -69,6 +69,7 @@ import { ACCOUNT_TEMPLATE_ADDRESS, TESTNET_NFT_FAUCET_ADDRESS, TESTNET_XTR_FAUCET_ADDRESS, + KeyBranch, } from "@tari-project/typescript-bindings"; import { settingsGet, submitTransactionDryRun, transactionsSubmit, transactionsWaitResult } from "@utils/json_rpc"; import { useAccountsList } from "@api/hooks/useAccounts"; @@ -207,12 +208,16 @@ function FlowEditor() { if (!account) { throw new Error("Account is not available"); } + if (!account.account.owner_key_id) { + throw new Error("Selected account does not have an owner key"); + } const request = { transaction: { V1: transaction }, - signing_key_id: account.account.owner_key_id, + seal_signer: { branch: "account" as KeyBranch, key_id: account.account.owner_key_id! }, + other_signers: [], detect_inputs: true, detect_inputs_use_unversioned: true, - proof_ids: [], + lock_ids: [], }; const submitResp = transaction.dry_run ? await submitTransactionDryRun(request) : await transactionsSubmit(request); const result = await transactionsWaitResult({ diff --git a/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx index 946e979b0f..0d6dd0dc5d 100644 --- a/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Settings/Components/DecryptUtxoBalance.tsx @@ -28,24 +28,24 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import { Divider } from "@mui/material"; import { stealthDecryptUtxoBalance } from "../../../utils/json_rpc"; -import { StealthUtxosDecryptValueRequest } from "@tari-project/typescript-bindings"; +import { StealthUtxosDecryptValueRequest, StealthUtxosDecryptValueResponse } from "@tari-project/typescript-bindings"; function DecryptUtxoBalanceForm() { const [formState, setFormState] = useState({ resourceAddress: null, utxoId: null, minimumExpectedValue: null, - maximumExpectedValue: null, + maximumExpectedValue: 100000000, keyId: 0, }); - const [balance, setBalance] = useState(null); + const [balance, setBalance] = useState(null); const onViewBalanceClicked = async () => { const resp = await stealthDecryptUtxoBalance({ resource_address: formState.resourceAddress!, ids: [formState.utxoId!], minimum_expected_value: formState.minimumExpectedValue ? BigInt(formState.minimumExpectedValue) : null, - maximum_expected_value: formState.maximumExpectedValue ? BigInt(formState.maximumExpectedValue) : null, + maximum_expected_value: BigInt(formState.maximumExpectedValue), view_key_id: BigInt(formState.keyId), } as StealthUtxosDecryptValueRequest); @@ -54,11 +54,11 @@ function DecryptUtxoBalanceForm() { const balances = balance && - Object.keys(balance?.balances).map((key) => { + Object.keys(balance?.values).map((key) => { return ( - {key}: {balance.balances[key] || "Failed not decrypt value"} + {key}: {balance.values[key]?.toString() || "Failed not decrypt value"} ); diff --git a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx index 1fb116dce3..78af0e8d53 100644 --- a/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx +++ b/applications/tari_walletd/web_ui/src/routes/StealthUtxoList/StealthUtxoList.tsx @@ -13,7 +13,7 @@ import { } from "@mui/material"; import { useState } from "react"; import { useStealthUtxosList } from "@/services/api/hooks/useAccounts"; -import { Account, OutputStatus, ResourceAddress, XTR } from "@tari-project/typescript-bindings"; +import { Account, OutputStatus, XTR } from "@tari-project/typescript-bindings"; import FetchStatusCheck from "@/components/FetchStatusCheck"; import { DataTableCell } from "@components/StyledComponents"; import StatusChip from "./components/StatusChip"; @@ -29,7 +29,6 @@ import { import CopyToClipboard from "@components/CopyToClipboard"; import PlaceHolder from "./components/PlaceHolder"; import SortableHeader from "./components/SortableHeader"; -import useCurrencyStore from "@store/currencyStore"; import { useParams } from "react-router-dom"; import { Memo } from "@components/Memo"; @@ -43,6 +42,7 @@ function StealthUtxoList({ account }: { account: Account }) { const resourceBalance = balancesData?.balances?.find((balance) => balance.resource_address === resourceAddress); const currencySymbol = resourceBalance ? resourceBalance.token_symbol || "" : ""; + const divisibility = resourceBalance ? resourceBalance.divisibility : 6; const getStatusDisplayName = (status: OutputStatus | "all") => { switch (status) { @@ -104,7 +104,7 @@ function StealthUtxoList({ account }: { account: Account }) { - {bigintToDecimalString(utxo.value, 6)} {currencySymbol} + {bigintToDecimalString(utxo.value, divisibility)} {currencySymbol} diff --git a/applications/tari_walletd/web_ui/src/routes/Transactions/Inputs.tsx b/applications/tari_walletd/web_ui/src/routes/Transactions/Inputs.tsx index a0831e354b..7efa719df5 100644 --- a/applications/tari_walletd/web_ui/src/routes/Transactions/Inputs.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Transactions/Inputs.tsx @@ -33,6 +33,8 @@ function getSubstateType(substateId: string): string { if (substateId.startsWith("commitment_")) return "Commitment"; if (substateId.startsWith("txreceipt_")) return "Transaction Receipt"; if (substateId.startsWith("template_")) return "Template"; + if (substateId.startsWith("utxo_")) return "Utxo"; + if (substateId.startsWith("vnfp_")) return "VnFeePool"; return "Unknown"; } @@ -50,6 +52,12 @@ function getTypeColor(type: string): "primary" | "secondary" | "success" | "warn return "warning"; case "Transaction Receipt": return "error"; + case "Template": + return "info"; + case "Utxo": + return "success"; + case "VnFeePool": + return "secondary"; default: return "primary"; } diff --git a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts index f493100674..eec9ee1239 100644 --- a/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts +++ b/applications/tari_walletd/web_ui/src/services/api/hooks/useAccounts.ts @@ -45,12 +45,13 @@ import { ClaimBurnRequest, ComponentAddress, ComponentAddressOrName, - ConfidentialTransferInputSelection, + UtxoInputSelection, decodeOotleAddress, Memo, OutputStatus, ResourceAddress, ResourceType, + XTR, } from "@tari-project/typescript-bindings"; const DEFAULT_MAX_FEE = 2000; @@ -124,7 +125,7 @@ export interface TransferParams { max_fee: number | null; resourceType: ResourceType; output_to_revealed: boolean; - input_selection: ConfidentialTransferInputSelection; + input_selection: UtxoInputSelection; badge_usage: BadgeUsage; dry_run: boolean; output_memo?: Memo; @@ -157,6 +158,8 @@ export const useAccountsTransfer = () => { } else if (params.resourceType === "Stealth") { let transferRequest = { owner_account: account, + // For simplicity, we'll use prefer revealed for fees whenever a non-XTR stealth transfer is made + fee_input_selection: params.resource_address === XTR ? params.input_selection : "PreferRevealed", input_selection: params.input_selection, resource_address: params.resource_address, badge_usage: params.badge_usage, diff --git a/applications/tari_walletd/web_ui/src/utils/json_rpc.ts b/applications/tari_walletd/web_ui/src/utils/json_rpc.ts index 9d79858cdc..c91acc26bf 100644 --- a/applications/tari_walletd/web_ui/src/utils/json_rpc.ts +++ b/applications/tari_walletd/web_ui/src/utils/json_rpc.ts @@ -109,6 +109,7 @@ import type { WebRtcStartResponse, StealthUtxosDecryptValueRequest, StealthUtxosDecryptValueResponse, + TransactionSubmitDryRunResponse, } from "@tari-project/typescript-bindings"; import { WalletDaemonClient } from "@tari-project/wallet_jrpc_client"; import useAuthStore from "@store/authStore"; @@ -252,7 +253,7 @@ export const keysSetActive = (request: KeysSetActiveRequest): Promise => client().then((c) => c.submitTransaction(request)); -export const submitTransactionDryRun = (request: TransactionSubmitRequest): Promise => +export const submitTransactionDryRun = (request: TransactionSubmitRequest): Promise => client().then((c) => c.submitTransactionDryRun(request)); export const transactionsGet = (request: TransactionGetRequest): Promise => client().then((c) => c.transactionsGet(request)); diff --git a/bindings/package.json b/bindings/package.json index 54d8d29e69..bbe206af50 100644 --- a/bindings/package.json +++ b/bindings/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/typescript-bindings", - "version": "1.19.0", + "version": "1.20.0", "description": "TypeScript types synchronized to the Tari Ootle Rust codebase", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/bindings/src/index.ts b/bindings/src/index.ts index 08c5e39fa7..88d400d69e 100644 --- a/bindings/src/index.ts +++ b/bindings/src/index.ts @@ -29,7 +29,6 @@ export * from "./types/ComponentCall"; export * from "./types/ComponentHeader"; export * from "./types/ComponentKey"; export * from "./types/ConfidentialOutputStatement"; -export * from "./types/ConfidentialTransferInputSelection"; export * from "./types/ConfidentialWithdrawProof"; export * from "./types/Decision"; export * from "./types/DiffSummary"; @@ -151,6 +150,7 @@ export * from "./types/UtxoAddressContents"; export * from "./types/UtxoAddress"; export * from "./types/UtxoBurnt"; export * from "./types/UtxoId"; +export * from "./types/UtxoInputSelection"; export * from "./types/UtxoOutput"; export * from "./types/UtxoSpent"; export * from "./types/UtxoStateUpdateSet"; diff --git a/bindings/src/tari-indexer-client.ts b/bindings/src/tari-indexer-client.ts index aeb4525407..5f6052c451 100644 --- a/bindings/src/tari-indexer-client.ts +++ b/bindings/src/tari-indexer-client.ts @@ -5,7 +5,6 @@ export * from "./types/tari-indexer-client/IndexerSubmitTransactionResponse"; export * from "./types/tari-indexer-client/GetTransactionReceiptResponse"; export * from "./types/tari-indexer-client/GetTemplateDefinitionRequest"; export * from "./types/tari-indexer-client/TemplateMetadata"; -export * from "./types/tari-indexer-client/GetUnspentUtxosResponse"; export * from "./types/tari-indexer-client/GetUtxoUpdatesRequest"; export * from "./types/tari-indexer-client/ListSubstatesResponse"; export * from "./types/tari-indexer-client/IndexerAddPeerResponse"; @@ -21,9 +20,12 @@ export * from "./types/tari-indexer-client/IndexerGetCommsStatsResponse"; export * from "./types/tari-indexer-client/IndexerConnection"; export * from "./types/tari-indexer-client/GetNonFungiblesRequest"; export * from "./types/tari-indexer-client/ListTransactionReceiptsRequest"; +export * from "./types/tari-indexer-client/GetUtxosRequest"; export * from "./types/tari-indexer-client/ListRecentTransactionsResponse"; export * from "./types/tari-indexer-client/IndexerAddPeerRequest"; export * from "./types/tari-indexer-client/GetNetworkSyncStateResponse"; +export * from "./types/tari-indexer-client/GetUtxosResponse"; +export * from "./types/tari-indexer-client/ListUtxosResponse"; export * from "./types/tari-indexer-client/GetNonFungiblesResponse"; export * from "./types/tari-indexer-client/GetTemplateDefinitionResponse"; export * from "./types/tari-indexer-client/ListTransactionReceiptsResponse"; @@ -40,8 +42,8 @@ export * from "./types/tari-indexer-client/NonFungibleSubstate"; export * from "./types/tari-indexer-client/ListTemplatesRequest"; export * from "./types/tari-indexer-client/IndexerGetTransactionResultResponse"; export * from "./types/tari-indexer-client/IndexerConnectionDirection"; -export * from "./types/tari-indexer-client/GetUnspentUtxosRequest"; export * from "./types/tari-indexer-client/IndexerGetIdentityResponse"; +export * from "./types/tari-indexer-client/ListUtxosRequest"; export * from "./types/tari-indexer-client/SyncProgress"; export * from "./types/tari-indexer-client/IndexerGetTransactionResultRequest"; export * from "./types/tari-indexer-client/IndexerReadyResponse"; diff --git a/bindings/src/types/ConfidentialTransferInputSelection.ts b/bindings/src/types/ConfidentialTransferInputSelection.ts deleted file mode 100644 index 4ad8a2bdb3..0000000000 --- a/bindings/src/types/ConfidentialTransferInputSelection.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ConfidentialTransferInputSelection = - | "ConfidentialOnly" - | "RevealedOnly" - | "PreferRevealed" - | "PreferConfidential"; diff --git a/bindings/src/types/StealthTransferStatement.ts b/bindings/src/types/StealthTransferStatement.ts index 3a7ac39051..2fc8f2a503 100644 --- a/bindings/src/types/StealthTransferStatement.ts +++ b/bindings/src/types/StealthTransferStatement.ts @@ -7,7 +7,7 @@ export type StealthTransferStatement = { outputs_statement: StealthOutputsStatement; /** * Balance proof that proves that no coins were created or destroyed during the transfer (assuming the range proof - * is valid). + * is valid). This may be None, if and only if, the transfer is revealed-only (i.e. no stealth inputs or outputs). */ - balance_proof: { public_nonce: string; signature: string }; + balance_proof: { public_nonce: string; signature: string } | null; }; diff --git a/bindings/src/types/UtxoInputSelection.ts b/bindings/src/types/UtxoInputSelection.ts new file mode 100644 index 0000000000..bb736d238e --- /dev/null +++ b/bindings/src/types/UtxoInputSelection.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UtxoInputSelection = "ConfidentialOnly" | "RevealedOnly" | "PreferRevealed" | "PreferConfidential"; diff --git a/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts b/bindings/src/types/tari-indexer-client/GetUtxosRequest.ts similarity index 90% rename from bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts rename to bindings/src/types/tari-indexer-client/GetUtxosRequest.ts index a850889f59..889f7e8561 100644 --- a/bindings/src/types/tari-indexer-client/GetUnspentUtxosRequest.ts +++ b/bindings/src/types/tari-indexer-client/GetUtxosRequest.ts @@ -3,7 +3,7 @@ import type { ResourceAddress } from "../ResourceAddress"; import type { RistrettoPublicKeyBytes } from "../RistrettoPublicKeyBytes"; import type { UtxoTag } from "../UtxoTag"; -export type GetUnspentUtxosRequest = { +export type GetUtxosRequest = { tag_and_nonce_pairs: Array<[UtxoTag, RistrettoPublicKeyBytes]>; resource_address: ResourceAddress; }; diff --git a/bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts b/bindings/src/types/tari-indexer-client/GetUtxosResponse.ts similarity index 72% rename from bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts rename to bindings/src/types/tari-indexer-client/GetUtxosResponse.ts index bad0be52ae..d037f53d54 100644 --- a/bindings/src/types/tari-indexer-client/GetUnspentUtxosResponse.ts +++ b/bindings/src/types/tari-indexer-client/GetUtxosResponse.ts @@ -2,4 +2,4 @@ import type { Utxo } from "../Utxo"; import type { UtxoId } from "../UtxoId"; -export type GetUnspentUtxosResponse = { utxos: Array<[UtxoId, Utxo]> }; +export type GetUtxosResponse = { utxos: Array<[UtxoId, Utxo]> }; diff --git a/bindings/src/types/tari-indexer-client/ListUtxosRequest.ts b/bindings/src/types/tari-indexer-client/ListUtxosRequest.ts new file mode 100644 index 0000000000..cdc6034923 --- /dev/null +++ b/bindings/src/types/tari-indexer-client/ListUtxosRequest.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResourceAddress } from "../ResourceAddress"; +import type { UtxoId } from "../UtxoId"; + +export type ListUtxosRequest = { resource_address: ResourceAddress; limit: number; from_id: UtxoId | null }; diff --git a/bindings/src/types/tari-indexer-client/ListUtxosResponse.ts b/bindings/src/types/tari-indexer-client/ListUtxosResponse.ts new file mode 100644 index 0000000000..e2cdc9bfea --- /dev/null +++ b/bindings/src/types/tari-indexer-client/ListUtxosResponse.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Utxo } from "../Utxo"; +import type { UtxoId } from "../UtxoId"; + +export type ListUtxosResponse = { utxos: Array<[UtxoId, Utxo]> }; diff --git a/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementRequest.ts b/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementRequest.ts new file mode 100644 index 0000000000..11b6f5eced --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { TransferStatementRequest } from "./TransferStatementRequest"; + +export type AccountsCreateStealthTransferStatementRequest = { requests: Array }; diff --git a/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementResponse.ts b/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementResponse.ts new file mode 100644 index 0000000000..dc1f0558fe --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/AccountsCreateStealthTransferStatementResponse.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { StealthTransferStatement } from "../StealthTransferStatement"; +import type { BranchAndKeyId } from "./BranchAndKeyId"; + +export type AccountsCreateStealthTransferStatementResponse = { + statements: Array; + lock_id: number; + signing_keys: Array; +}; diff --git a/bindings/src/types/wallet-daemon-client/BranchAndKeyId.ts b/bindings/src/types/wallet-daemon-client/BranchAndKeyId.ts new file mode 100644 index 0000000000..ff8f22eb86 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/BranchAndKeyId.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { KeyBranch } from "./KeyBranch"; +import type { KeyId } from "./KeyId"; + +export type BranchAndKeyId = { branch: KeyBranch; key_id: KeyId }; diff --git a/bindings/src/types/wallet-daemon-client/ConfidentialTransferRequest.ts b/bindings/src/types/wallet-daemon-client/ConfidentialTransferRequest.ts index 93247461b9..20d490246c 100644 --- a/bindings/src/types/wallet-daemon-client/ConfidentialTransferRequest.ts +++ b/bindings/src/types/wallet-daemon-client/ConfidentialTransferRequest.ts @@ -1,15 +1,15 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Amount } from "../Amount"; -import type { ConfidentialTransferInputSelection } from "../ConfidentialTransferInputSelection"; import type { Memo } from "../Memo"; import type { OotleAddress } from "../OotleAddress"; import type { ResourceAddress } from "../ResourceAddress"; +import type { UtxoInputSelection } from "../UtxoInputSelection"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; export type ConfidentialTransferRequest = { account: ComponentAddressOrName | null; amount: Amount; - input_selection: ConfidentialTransferInputSelection; + input_selection: UtxoInputSelection; resource_address: ResourceAddress; destination_address: OotleAddress; max_fee: number | null; diff --git a/bindings/src/types/wallet-daemon-client/InputSelection.ts b/bindings/src/types/wallet-daemon-client/InputSelection.ts new file mode 100644 index 0000000000..5cc72ef053 --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/InputSelection.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Amount } from "../Amount"; +import type { UtxoInputSelection } from "../UtxoInputSelection"; + +export type InputSelection = { FromBucket: { revealed_amount: Amount } } | { Selection: UtxoInputSelection }; diff --git a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts index 0b6d5347e3..4dd3621ba7 100644 --- a/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts +++ b/bindings/src/types/wallet-daemon-client/StealthTransferRequest.ts @@ -1,15 +1,16 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ConfidentialTransferInputSelection } from "../ConfidentialTransferInputSelection"; import type { ResourceAddress } from "../ResourceAddress"; +import type { UtxoInputSelection } from "../UtxoInputSelection"; import type { BadgeUsage } from "./BadgeUsage"; import type { ComponentAddressOrName } from "./ComponentAddressOrName"; import type { StealthTransfer } from "./StealthTransfer"; export type StealthTransferRequest = { owner_account: ComponentAddressOrName; - input_selection: ConfidentialTransferInputSelection; + fee_input_selection: UtxoInputSelection; + input_selection: UtxoInputSelection; resource_address: ResourceAddress; - badge_usage: BadgeUsage; + badge_usage?: BadgeUsage; transfers: Array; max_fee: number; dry_run: boolean; diff --git a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts index 3f8ea6e1a3..a6def9cf78 100644 --- a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts +++ b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueRequest.ts @@ -7,5 +7,5 @@ export type StealthUtxosDecryptValueRequest = { ids: Array; view_key_id: bigint; minimum_expected_value: bigint | null; - maximum_expected_value: bigint | null; + maximum_expected_value: bigint; }; diff --git a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts index 5c1c4ade01..ffcf08353f 100644 --- a/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts +++ b/bindings/src/types/wallet-daemon-client/StealthUtxosDecryptValueResponse.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { UtxoId } from "../UtxoId"; -export type StealthUtxosDecryptValueResponse = { balances: { [key in UtxoId]?: bigint | null } }; +export type StealthUtxosDecryptValueResponse = { values: { [key in UtxoId]?: bigint | null } }; diff --git a/bindings/src/types/wallet-daemon-client/TransactionSubmitDryRunRequest.ts b/bindings/src/types/wallet-daemon-client/TransactionSubmitDryRunRequest.ts deleted file mode 100644 index ae7655c4cb..0000000000 --- a/bindings/src/types/wallet-daemon-client/TransactionSubmitDryRunRequest.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { UnsignedTransaction } from "../UnsignedTransaction"; -import type { KeyId } from "./KeyId"; - -export type TransactionSubmitDryRunRequest = { - transaction: UnsignedTransaction; - signing_key_id: KeyId | null; - detect_inputs: boolean; - detect_inputs_use_unversioned: boolean; - proof_ids: Array; -}; diff --git a/bindings/src/types/wallet-daemon-client/TransactionSubmitRequest.ts b/bindings/src/types/wallet-daemon-client/TransactionSubmitRequest.ts index 02140988c7..89adbe83f7 100644 --- a/bindings/src/types/wallet-daemon-client/TransactionSubmitRequest.ts +++ b/bindings/src/types/wallet-daemon-client/TransactionSubmitRequest.ts @@ -1,10 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { UnsignedTransaction } from "../UnsignedTransaction"; -import type { KeyId } from "./KeyId"; +import type { BranchAndKeyId } from "./BranchAndKeyId"; export type TransactionSubmitRequest = { transaction: UnsignedTransaction; - signing_key_id: KeyId | null; + seal_signer: BranchAndKeyId; + other_signers: Array; /** * Attempt to infer inputs and their dependencies from instructions. If false, the provided transaction must * contain the required inputs. @@ -12,9 +13,9 @@ export type TransactionSubmitRequest = { detect_inputs: boolean; /** * If true(default), detected inputs will omit versions allowing consensus to resolve input substates. - * If false, the wallet will try determine versioned for the inputs. These may be outdated if the substate has + * If false, the wallet will try to determine versions for the inputs. These may be outdated if the substate has * changed since detection. */ detect_inputs_use_unversioned: boolean; - proof_ids: Array; + lock_ids: Array; }; diff --git a/bindings/src/types/wallet-daemon-client/TransferOutput.ts b/bindings/src/types/wallet-daemon-client/TransferOutput.ts new file mode 100644 index 0000000000..dd87db0d4c --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/TransferOutput.ts @@ -0,0 +1,24 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Amount } from "../Amount"; +import type { Memo } from "../Memo"; +import type { OotleAddress } from "../OotleAddress"; + +export type TransferOutput = { + /** + * Destination address used to derive the UTXO encryption keys, owner signature and the account in which to + * deposit revealed funds + */ + address: OotleAddress; + /** + * Amount to spend to a revealed output + */ + revealed_amount: Amount; + /** + * Amount to spend to a blinded output + */ + blinded_amount: Amount; + /** + * Optional memo to include a memo in the output. This memo is encrypted and can only be read by the recipient. + */ + memo: Memo | null; +}; diff --git a/bindings/src/types/wallet-daemon-client/TransferStatementRequest.ts b/bindings/src/types/wallet-daemon-client/TransferStatementRequest.ts new file mode 100644 index 0000000000..d8e763ac9e --- /dev/null +++ b/bindings/src/types/wallet-daemon-client/TransferStatementRequest.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ResourceAddress } from "../ResourceAddress"; +import type { ComponentAddressOrName } from "./ComponentAddressOrName"; +import type { InputSelection } from "./InputSelection"; +import type { TransferOutput } from "./TransferOutput"; + +export type TransferStatementRequest = { + sender_account: ComponentAddressOrName; + resource_address: ResourceAddress; + input_selection: InputSelection; + outputs: Array; +}; diff --git a/bindings/src/wallet-daemon-client.ts b/bindings/src/wallet-daemon-client.ts index f49266ec31..cd80687a16 100644 --- a/bindings/src/wallet-daemon-client.ts +++ b/bindings/src/wallet-daemon-client.ts @@ -22,6 +22,7 @@ export * from "./types/wallet-daemon-client/SettingsSetRequest"; export * from "./types/wallet-daemon-client/StealthTransferResponse"; export * from "./types/wallet-daemon-client/StealthTransferRequest"; export * from "./types/wallet-daemon-client/GetValidatorFeesResponse"; +export * from "./types/wallet-daemon-client/AccountsCreateStealthTransferStatementResponse"; export * from "./types/wallet-daemon-client/ClaimBurnProof"; export * from "./types/wallet-daemon-client/TransactionGetAllRequest"; export * from "./types/wallet-daemon-client/WebauthnAlreadyRegisteredRequest"; @@ -79,10 +80,12 @@ export * from "./types/wallet-daemon-client/AccountsCreateRequest"; export * from "./types/wallet-daemon-client/TransactionClaimBurnResponse"; export * from "./types/wallet-daemon-client/TransactionWaitResultRequest"; export * from "./types/wallet-daemon-client/ClaimValidatorFeesResponse"; +export * from "./types/wallet-daemon-client/BranchAndKeyId"; export * from "./types/wallet-daemon-client/ConfidentialCreateOutputProofRequest"; export * from "./types/wallet-daemon-client/KeysCreateRequest"; export * from "./types/wallet-daemon-client/WebauthnStartAuthRequest"; export * from "./types/wallet-daemon-client/NetworkInfo"; +export * from "./types/wallet-daemon-client/TransferStatementRequest"; export * from "./types/wallet-daemon-client/AccountsCreateOrGetRequest"; export * from "./types/wallet-daemon-client/SettingsGetResponse"; export * from "./types/wallet-daemon-client/WebauthnFinishRegisterResponse"; @@ -91,11 +94,13 @@ export * from "./types/wallet-daemon-client/WalletGetInfoRequest"; export * from "./types/wallet-daemon-client/StealthUtxosListRequest"; export * from "./types/wallet-daemon-client/ClaimBurnResponse"; export * from "./types/wallet-daemon-client/WebRtcStartRequest"; +export * from "./types/wallet-daemon-client/InputSelection"; export * from "./types/wallet-daemon-client/ConfidentialViewVaultBalanceResponse"; export * from "./types/wallet-daemon-client/WebRtcStart"; export * from "./types/wallet-daemon-client/AccountSetDefaultRequest"; export * from "./types/wallet-daemon-client/WebauthnStartRegisterResponse"; export * from "./types/wallet-daemon-client/ConfidentialCreateOutputProofResponse"; +export * from "./types/wallet-daemon-client/AccountsCreateStealthTransferStatementRequest"; export * from "./types/wallet-daemon-client/TemplatesGetResponse"; export * from "./types/wallet-daemon-client/AccountsGetBalancesRequest"; export * from "./types/wallet-daemon-client/AccountsCreateFreeTestCoinsRequest"; @@ -120,11 +125,11 @@ export * from "./types/wallet-daemon-client/KeysSetActiveResponse"; export * from "./types/wallet-daemon-client/ComponentAddressOrName"; export * from "./types/wallet-daemon-client/AuthGetMethodRequest"; export * from "./types/wallet-daemon-client/AuthLoginAcceptRequest"; -export * from "./types/wallet-daemon-client/TransactionSubmitDryRunRequest"; export * from "./types/wallet-daemon-client/KeysSetActiveRequest"; export * from "./types/wallet-daemon-client/ConfidentialViewVaultBalanceRequest"; export * from "./types/wallet-daemon-client/UtxoInfo"; export * from "./types/wallet-daemon-client/TemplatesListAuthoredResponse"; +export * from "./types/wallet-daemon-client/TransferOutput"; export * from "./types/wallet-daemon-client/ConfidentialTransferRequest"; export * from "./types/wallet-daemon-client/AuthoredTemplate"; export * from "./types/wallet-daemon-client/AccountsCreateResponse"; diff --git a/clients/javascript/indexer_client/package.json b/clients/javascript/indexer_client/package.json index 04a31b4d96..29b8959e25 100644 --- a/clients/javascript/indexer_client/package.json +++ b/clients/javascript/indexer_client/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/indexer-client", - "version": "1.0.1", + "version": "1.1.0", "description": "Tari indexer REST API client library", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/clients/javascript/wallet_daemon_client/package.json b/clients/javascript/wallet_daemon_client/package.json index d064f87fb5..273aa8b7fe 100644 --- a/clients/javascript/wallet_daemon_client/package.json +++ b/clients/javascript/wallet_daemon_client/package.json @@ -1,6 +1,6 @@ { "name": "@tari-project/wallet_jrpc_client", - "version": "1.10.0", + "version": "1.11.0", "description": "Tari wallet JSON-RPC client library", "homepage": "https://github.com/tari-project/tari-ootle#readme", "bugs": { diff --git a/clients/javascript/wallet_daemon_client/src/index.ts b/clients/javascript/wallet_daemon_client/src/index.ts index 28e7a71989..d4fe955fb1 100644 --- a/clients/javascript/wallet_daemon_client/src/index.ts +++ b/clients/javascript/wallet_daemon_client/src/index.ts @@ -77,7 +77,6 @@ import type { TransactionGetResponse, TransactionGetResultRequest, TransactionGetResultResponse, - TransactionSubmitDryRunRequest, TransactionSubmitDryRunResponse, TransactionSubmitManifestRequest, TransactionSubmitManifestResponse, @@ -226,7 +225,7 @@ export class WalletDaemonClient { return this.__invokeRpc("transactions.submit", params); } - public submitTransactionDryRun(params: TransactionSubmitDryRunRequest): Promise { + public submitTransactionDryRun(params: TransactionSubmitRequest): Promise { return this.__invokeRpc("transactions.submit_dry_run", params); } diff --git a/clients/tari_indexer_client/src/rest_api_client.rs b/clients/tari_indexer_client/src/rest_api_client.rs index bdb0e4f199..46ff49a421 100644 --- a/clients/tari_indexer_client/src/rest_api_client.rs +++ b/clients/tari_indexer_client/src/rest_api_client.rs @@ -26,9 +26,9 @@ use crate::{ GetTransactionReceiptResponse, GetTransactionResultRequest, GetTransactionResultResponse, - GetUnspentUtxosRequest, - GetUnspentUtxosResponse, GetUtxoUpdatesRequest, + GetUtxosRequest, + GetUtxosResponse, IndexerReadyResponse, ListRecentTransactionsRequest, ListRecentTransactionsResponse, @@ -36,6 +36,8 @@ use crate::{ ListSubstatesResponse, ListTransactionReceiptsRequest, ListTransactionReceiptsResponse, + ListUtxosRequest, + ListUtxosResponse, SubmitTransactionRequest, SubmitTransactionResponse, }, @@ -165,13 +167,14 @@ impl IndexerRestApiClient { Ok(stream) } - pub async fn get_unspent_utxos( - &mut self, - req: GetUnspentUtxosRequest, - ) -> Result { + pub async fn get_utxos(&mut self, req: GetUtxosRequest) -> Result { self.send_post("utxos/fetch", req).await } + pub async fn list_utxos(&mut self, req: ListUtxosRequest) -> Result { + self.send_get("utxos", req).await + } + pub async fn list_transaction_receipts( &mut self, req: ListTransactionReceiptsRequest, diff --git a/clients/tari_indexer_client/src/types.rs b/clients/tari_indexer_client/src/types.rs index a0f105d25b..d094d5b0f9 100644 --- a/clients/tari_indexer_client/src/types.rs +++ b/clients/tari_indexer_client/src/types.rs @@ -388,14 +388,28 @@ pub struct GetUtxoUpdatesResponse { #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] -pub struct GetUnspentUtxosRequest { +pub struct GetUtxosRequest { pub tag_and_nonce_pairs: Vec<(UtxoTag, RistrettoPublicKeyBytes)>, pub resource_address: ResourceAddress, } #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] -pub struct GetUnspentUtxosResponse { +pub struct GetUtxosResponse { + pub utxos: Vec<(UtxoId, Utxo)>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct ListUtxosRequest { + pub resource_address: ResourceAddress, + pub limit: u32, + pub from_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "tari-indexer-client/"))] +pub struct ListUtxosResponse { pub utxos: Vec<(UtxoId, Utxo)>, } diff --git a/clients/wallet_daemon_client/src/component_address.rs b/clients/wallet_daemon_client/src/component_address.rs index 91197b6ab6..2144ce2a64 100644 --- a/clients/wallet_daemon_client/src/component_address.rs +++ b/clients/wallet_daemon_client/src/component_address.rs @@ -65,3 +65,8 @@ impl From for ComponentAddressOrName { Self::Name(name) } } +impl From<&str> for ComponentAddressOrName { + fn from(name: &str) -> Self { + Self::Name(name.to_string()) + } +} diff --git a/clients/wallet_daemon_client/src/lib.rs b/clients/wallet_daemon_client/src/lib.rs index ce8383e03e..2c68ad09bd 100644 --- a/clients/wallet_daemon_client/src/lib.rs +++ b/clients/wallet_daemon_client/src/lib.rs @@ -82,6 +82,8 @@ use crate::{ AccountsCreateOrGetResponse, AccountsCreateRequest, AccountsCreateResponse, + AccountsCreateStealthTransferStatementRequest, + AccountsCreateStealthTransferStatementResponse, AccountsGetBalancesRequest, AccountsGetBalancesResponse, AccountsListRequest, @@ -161,6 +163,10 @@ impl WalletDaemonClient { }) } + pub fn endpoint(&self) -> &Url { + &self.endpoint + } + pub fn set_auth_token(&mut self, token: EncodedJwtString) -> &mut Self { self.token = Some(token); self @@ -352,6 +358,16 @@ impl WalletDaemonClient { self.send_request("accounts.stealth_transfer", req.borrow()).await } + pub async fn accounts_create_stealth_transfer_statement< + T: Borrow, + >( + &mut self, + req: T, + ) -> Result { + self.send_request("accounts.create_stealth_transfer_statement", req.borrow()) + .await + } + pub async fn claim_burn>( &mut self, req: T, @@ -359,7 +375,7 @@ impl WalletDaemonClient { self.send_request("accounts.claim_burn", req.borrow()).await } - pub async fn create_transfer_proof>( + pub async fn confidential_create_transfer_proof>( &mut self, req: T, ) -> Result { diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index 659fb5ef7b..f53b941caf 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -39,11 +39,15 @@ use tari_ootle_common_types::{ SubstateRequirement, }; use tari_ootle_wallet_sdk::{ - apis::{confidential_transfer::ConfidentialTransferInputSelection, stealth_transfer::BadgeUsage}, + apis::{ + confidential_transfer::UtxoInputSelection, + stealth_transfer::{BadgeUsage, TransferOutput}, + }, crypto::memo::Memo, models::{ Account, AuthoredTemplateModel, + BranchAndKeyId, DerivedKeyIndex, KeyBranch, KeyId, @@ -56,7 +60,15 @@ use tari_ootle_wallet_sdk::{ }; use tari_template_abi::{FunctionDef, TemplateDef}; use tari_template_lib::{ - models::{ConfidentialOutputStatement, NonFungibleId, ResourceAddress, UtxoAddress, UtxoId, VaultId}, + models::{ + ConfidentialOutputStatement, + NonFungibleId, + ResourceAddress, + StealthTransferStatement, + UtxoAddress, + UtxoId, + VaultId, + }, prelude::{ComponentAddress, ConfidentialWithdrawProof, ResourceType, RistrettoPublicKeyBytes}, types::{crypto::PedersenCommitmentBytes, Amount, EncryptedData, TemplateAddress}, }; @@ -103,21 +115,21 @@ pub struct CallInstructionRequest { pub max_epoch: Option, } -#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct TransactionSubmitRequest { pub transaction: UnsignedTransaction, - pub signing_key_id: Option, + pub seal_signer: BranchAndKeyId, + pub other_signers: Vec, /// Attempt to infer inputs and their dependencies from instructions. If false, the provided transaction must /// contain the required inputs. pub detect_inputs: bool, /// If true(default), detected inputs will omit versions allowing consensus to resolve input substates. - /// If false, the wallet will try determine versioned for the inputs. These may be outdated if the substate has + /// If false, the wallet will try to determine versions for the inputs. These may be outdated if the substate has /// changed since detection. #[serde(default = "return_true")] pub detect_inputs_use_unversioned: bool, - #[cfg_attr(feature = "ts", ts(type = "Array"))] - pub proof_ids: Vec, + pub lock_ids: Vec, } const fn return_true() -> bool { @@ -130,16 +142,7 @@ pub struct TransactionSubmitResponse { pub transaction_id: TransactionId, } -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] -pub struct TransactionSubmitDryRunRequest { - pub transaction: UnsignedTransaction, - pub signing_key_id: Option, - pub detect_inputs: bool, - pub detect_inputs_use_unversioned: bool, - #[cfg_attr(feature = "ts", ts(type = "Array"))] - pub proof_ids: Vec, -} +pub type TransactionSubmitDryRunRequest = TransactionSubmitRequest; #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] @@ -541,7 +544,7 @@ pub struct ConfidentialTransferRequest { #[serde(deserialize_with = "opt_string_or_struct")] pub account: Option, pub amount: Amount, - pub input_selection: ConfidentialTransferInputSelection, + pub input_selection: UtxoInputSelection, pub resource_address: ResourceAddress, pub destination_address: OotleAddress, #[cfg_attr(feature = "ts", ts(type = "number | null"))] @@ -1055,11 +1058,64 @@ pub struct TransferNftResponse { pub result: FinalizeResult, } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct AccountsCreateStealthTransferStatementRequest { + pub requests: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct TransferStatementRequest { + pub sender_account: ComponentAddressOrName, + pub resource_address: ResourceAddress, + pub input_selection: InputSelection, + pub outputs: Vec, +} + +impl TransferStatementRequest { + pub fn total_output_amount(&self) -> Amount { + self.outputs.iter().map(|o| o.blinded_amount + o.revealed_amount).sum() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub enum InputSelection { + FromBucket { revealed_amount: Amount }, + Selection(UtxoInputSelection), +} + +impl InputSelection { + pub fn as_selection(&self) -> Option { + match self { + InputSelection::FromBucket { .. } => None, + InputSelection::Selection(s) => Some(*s), + } + } + + pub fn as_from_bucket(&self) -> Option { + match self { + InputSelection::FromBucket { revealed_amount } => Some(*revealed_amount), + InputSelection::Selection(_) => None, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct AccountsCreateStealthTransferStatementResponse { + pub statements: Vec, + pub lock_id: WalletLockId, + pub signing_keys: Vec, +} + #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct StealthTransferRequest { pub owner_account: ComponentAddressOrName, - pub input_selection: ConfidentialTransferInputSelection, + pub fee_input_selection: UtxoInputSelection, + pub input_selection: UtxoInputSelection, pub resource_address: ResourceAddress, #[serde(default, skip_serializing_if = "BadgeUsage::is_none")] pub badge_usage: BadgeUsage, @@ -1129,11 +1185,11 @@ pub struct StealthUtxosDecryptValueRequest { pub ids: Vec, pub view_key_id: u64, pub minimum_expected_value: Option, - pub maximum_expected_value: Option, + pub maximum_expected_value: u64, } #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct StealthUtxosDecryptValueResponse { - pub balances: HashMap>, + pub values: HashMap>, } diff --git a/crates/engine_types/src/crypto/elgamal.rs b/crates/engine_types/src/crypto/elgamal.rs index 47583a707a..e13b0a004e 100644 --- a/crates/engine_types/src/crypto/elgamal.rs +++ b/crates/engine_types/src/crypto/elgamal.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use borsh::BorshSerialize; +use log::{info, warn}; use tari_bor::{Deserialize, Serialize}; use tari_common_types::types::PrivateKey; use tari_crypto::{ @@ -20,6 +21,8 @@ use crate::{ ToByteType, }; +const LOG_TARGET: &str = "tari::ootle::engine_types::crypto::elgamal"; + pub fn validate_elgamal_verifiable_balance_proof( commitment: &PedersenCommitment, view_key: Option<&RistrettoPublicKey>, @@ -207,9 +210,18 @@ impl ElgamalVerifiableBalance { .collect::>(); let mut results = vec![None; balances.len()]; + let mut warning_logged = false; for v in value_range { let value = lookup_table.lookup(v)?.unwrap_or_else(|| { + if !warning_logged { + warning_logged = true; + warn!( + target: LOG_TARGET, + "Value lookup table missing entry for value {}, falling back to slow lookup method", + v + ); + } // Fallback to slow lookup method if the lookup table does not contain a key for the value let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); copy_fixed(pk.as_bytes()) @@ -217,6 +229,7 @@ impl ElgamalVerifiableBalance { while let Some(pos) = balances.iter().position(|(_, balance)| value == balance.as_bytes()) { let (order, _) = balances.swap_remove(pos); + info!(target: LOG_TARGET, "Found encrypted balance: {}", v); results .get_mut(order) .expect("batched_brute_force: balances index greater than results") diff --git a/crates/engine_types/src/stealth/transfer.rs b/crates/engine_types/src/stealth/transfer.rs index d163db3c27..2e06919dfb 100644 --- a/crates/engine_types/src/stealth/transfer.rs +++ b/crates/engine_types/src/stealth/transfer.rs @@ -4,7 +4,7 @@ use log::*; use tari_crypto::{ ristretto::{pedersen::PedersenCommitment, RistrettoPublicKey, RistrettoSecretKey}, - tari_utilities::{ByteArray, ByteArrayError}, + tari_utilities::ByteArrayError, }; use tari_template_lib::{ models::{StealthInput, StealthTransferStatement}, @@ -37,10 +37,15 @@ pub fn validate_transfer_balance( basic_validations(transfer)?; let validated_outputs = stealth::validate_stealth_outputs_statement(&transfer.outputs_statement, view_key)?; - let balance_proof = - try_decode_to_signature(&transfer.balance_proof).ok_or_else(|| ResourceError::InvalidBalanceProof { - details: "Malformed balance proof".to_string(), - })?; + let balance_proof = transfer + .balance_proof + .as_ref() + .map(|s| { + try_decode_to_signature(s).ok_or_else(|| ResourceError::InvalidBalanceProof { + details: "Malformed balance proof".to_string(), + }) + }) + .transpose()?; // EDGE CASE: If there are no inputs and no outputs, the public excess will be 0.G if transfer.inputs_statement.inputs.is_empty() && validated_outputs.is_empty() { @@ -51,12 +56,13 @@ pub fn validate_transfer_balance( }); } - // In this case, the public excess is 0 (s = r + e.0 = r) which leaks the secret nonce. So instead, we enforce - // that a zero signature MUST be used for this case. - if balance_proof.get_signature().as_bytes() != RistrettoSecretKey::default().as_bytes() { + // In this case, the public excess is 0 (s = r + e.0 = r) which leaks the secret nonce. Probably fine, but + // instead, we enforce that a None signature MUST be used for this case. + if balance_proof.is_some() { return Err(ResourceError::InvalidBalanceProof { - details: "Balance proof signature verification failed for revealed amount. This typically indicates \ - that the transfer statement total input amount != total output amount." + details: "Balance proof signature verification failed for revealed amount. This indicates that the \ + transfer statement provided a balance proof when there are no stealth inputs or outputs, \ + None is required." .to_string(), }); } @@ -66,6 +72,9 @@ pub fn validate_transfer_balance( revealed_output_amount: transfer.outputs_statement.revealed_output_amount, }); } + let balance_proof = balance_proof.ok_or_else(|| ResourceError::InvalidBalanceProof { + details: "Balance proof must be provided when there are stealth inputs or outputs".to_string(), + })?; let agg_outputs = validated_outputs.iter().fold(RistrettoPublicKey::default(), |acc, o| { acc + o.output.commitment.as_public_key() @@ -206,9 +215,12 @@ fn basic_validations(transfer: &StealthTransferStatement) -> Result<(), Resource ), }); } - } else if transfer.balance_proof.public_nonce().is_zero() || transfer.balance_proof.signature().is_zero() { + } else if transfer + .balance_proof + .is_none_or(|p| p.public_nonce().is_zero() || p.signature().is_zero()) + { return Err(ResourceError::InvalidBalanceProof { - details: "Balance proof public nonce and signature cannot be zero".to_string(), + details: "Balance proof must be provided and public nonce and signature cannot be zero".to_string(), }); } else { // Ok diff --git a/crates/epoch_oracles/src/base_layer/mod.rs b/crates/epoch_oracles/src/base_layer/mod.rs index 0e42c08c99..8221331120 100644 --- a/crates/epoch_oracles/src/base_layer/mod.rs +++ b/crates/epoch_oracles/src/base_layer/mod.rs @@ -156,7 +156,7 @@ impl BaseLayerOracleInner { target: LOG_TARGET, "⚠️ Base layer reorg detected. Rescanning from genesis." ); - // TODO: we need to figure out where the fork happened, and delete data after the fork. + // TODO: we need to figure out where the fork happened, and delete data after the fork if able. self.last_scanned_hash = None; self.last_scanned_validator_node_mr = None; self.last_scanned_height = 0; diff --git a/crates/epoch_oracles/src/configured/real_time_ticker.rs b/crates/epoch_oracles/src/configured/real_time_ticker.rs index 67de835392..a282db4e6a 100644 --- a/crates/epoch_oracles/src/configured/real_time_ticker.rs +++ b/crates/epoch_oracles/src/configured/real_time_ticker.rs @@ -63,6 +63,7 @@ impl EpochTicker for RealTimeEpochTicker { fn poll_tick(&mut self, cx: &mut Context) -> Poll> { let calculated_epoch = self.calc_current_epoch(); + // Emit quickly to the initial epoch if calculated_epoch < self.initial_epoch { let epoch = self.increment_epoch(); let epoch_hash = calc_static_epoch_hash(epoch); @@ -73,6 +74,7 @@ impl EpochTicker for RealTimeEpochTicker { })); } + // Emit quickly to catch up to the calculated epoch if calculated_epoch > self.epoch { let epoch = self.increment_epoch(); let epoch_hash = calc_static_epoch_hash(epoch); @@ -86,9 +88,10 @@ impl EpochTicker for RealTimeEpochTicker { if let Some(interval_mut) = self.interval.as_mut() { loop { + // Every tick, check if we need to emit a new epoch ready!(interval_mut.poll_tick(cx)); - if self.epoch.is_zero() || self.epoch <= calculated_epoch { + if self.epoch <= calculated_epoch { let epoch = self.increment_epoch(); let epoch_hash = calc_static_epoch_hash(epoch); return Poll::Ready(Some(EpochTickerData { @@ -157,4 +160,19 @@ mod tests { ); } } + + #[tokio::test] + async fn it_stays_on_the_current_epoch_of_base_time_in_future() { + let base_time = time::OffsetDateTime::now_utc() + time::Duration::seconds(1); + let mut ticker = + RealTimeEpochTicker::new(Epoch(0), base_time, Epoch(1)).with_epoch_time_secs(1.try_into().unwrap()); + + for i in 0..3 { + let res = timeout(Duration::from_secs(10), poll_fn(|cx| ticker.poll_tick(cx))) + .await + .expect("elapsed") + .unwrap(); + assert_eq!(res.epoch, Epoch(i + 1)); + } + } } diff --git a/crates/p2p/src/conversions/transaction.rs b/crates/p2p/src/conversions/transaction.rs index 186d489d66..4d596bb067 100644 --- a/crates/p2p/src/conversions/transaction.rs +++ b/crates/p2p/src/conversions/transaction.rs @@ -806,8 +806,11 @@ impl TryFrom for tari_template_lib .outputs_statement .ok_or_else(|| anyhow!("output_statement not provided"))? .try_into()?, - balance_proof: BalanceProofSignature::from_bytes(&value.balance_proof) - .map_err(|e| anyhow!("Invalid balance proof signature: {}", e))?, + balance_proof: Some(&value.balance_proof) + .filter(|b| !b.is_empty()) + .map(|b| BalanceProofSignature::from_bytes(b)) + .transpose() + .context("Invalid balance proof signature")?, }) } } @@ -817,7 +820,7 @@ impl From for proto::transa Self { inputs_statement: Some(value.inputs_statement.into()), outputs_statement: Some(value.outputs_statement.into()), - balance_proof: value.balance_proof.to_bytes(), + balance_proof: value.balance_proof.as_ref().map(|b| b.to_bytes()).unwrap_or_default(), } } } diff --git a/crates/template_builtin/templates/faucet/src/lib.rs b/crates/template_builtin/templates/faucet/src/lib.rs index 0df5f6ff1a..d75a0f5210 100644 --- a/crates/template_builtin/templates/faucet/src/lib.rs +++ b/crates/template_builtin/templates/faucet/src/lib.rs @@ -24,18 +24,17 @@ mod template { &self, amount: Amount, output: StealthOutputsStatement, - balance_proof: BalanceProofSignature, - required_signer: RistrettoPublicKeyBytes, + balance_proof: Option, ) -> Option { + let signer = CallerContext::transaction_signer_public_key(); let revealed_bucket = self.vault.withdraw(amount); let transfer = StealthTransferStatement { - inputs_statement: StealthInputsStatement::new_revealed_only(amount, required_signer), + inputs_statement: StealthInputsStatement::new_revealed_only(amount, signer), outputs_statement: output, balance_proof, }; debug!("Withdrawing {} coins from faucet into confidential output", amount); - let signer = CallerContext::transaction_signer_public_key(); emit_event("take", [ ("amount", amount.to_string()), ("confidential", "true".to_string()), diff --git a/crates/template_lib/src/models/stealth.rs b/crates/template_lib/src/models/stealth.rs index 7d282a1261..ce500e56b6 100644 --- a/crates/template_lib/src/models/stealth.rs +++ b/crates/template_lib/src/models/stealth.rs @@ -30,6 +30,17 @@ pub struct StealthOutputsStatement { pub agg_range_proof: RangeProofBytes, } +impl StealthOutputsStatement { + /// Create a new output statement with no stealth outputs, only a revealed amount. + pub fn new_revealed_only(amount: Amount) -> Self { + Self { + outputs: vec![], + revealed_output_amount: amount, + agg_range_proof: RangeProofBytes::empty(), + } + } +} + /// A statement for stealth outputs. A statement must contain confidential outputs #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] @@ -81,12 +92,24 @@ pub struct StealthTransferStatement { pub inputs_statement: StealthInputsStatement, pub outputs_statement: StealthOutputsStatement, /// Balance proof that proves that no coins were created or destroyed during the transfer (assuming the range proof - /// is valid). - #[cfg_attr(feature = "ts", ts(type = "{public_nonce: string, signature: string}"))] - pub balance_proof: BalanceProofSignature, + /// is valid). This may be None, if and only if, the transfer is revealed-only (i.e. no stealth inputs or outputs). + #[cfg_attr(feature = "ts", ts(type = "{public_nonce: string, signature: string} | null"))] + pub balance_proof: Option, } impl StealthTransferStatement { + pub fn revealed_only( + input_amount: Amount, + output_amount: Amount, + required_signer: RistrettoPublicKeyBytes, + ) -> Self { + Self { + inputs_statement: StealthInputsStatement::new_revealed_only(input_amount, required_signer), + outputs_statement: StealthOutputsStatement::new_revealed_only(output_amount), + balance_proof: None, + } + } + pub fn revealed_input_amount(&self) -> Amount { self.inputs_statement.revealed_amount } diff --git a/crates/template_test_tooling/src/support/stealth.rs b/crates/template_test_tooling/src/support/stealth.rs index 86fed86f96..28c6e6e679 100644 --- a/crates/template_test_tooling/src/support/stealth.rs +++ b/crates/template_test_tooling/src/support/stealth.rs @@ -215,9 +215,9 @@ fn generate_transfer_data_internal, A: Into>( .collect::>(); let transfer = stealth::create_transfer_statement( - &inputs, + inputs.iter(), revealed_input_amount.into(), - &outputs, + outputs.iter(), revealed_output_amount.into(), required_signer, ) diff --git a/crates/transaction/src/v1/unsigned.rs b/crates/transaction/src/v1/unsigned.rs index f131b19e54..b74da0b7a0 100644 --- a/crates/transaction/src/v1/unsigned.rs +++ b/crates/transaction/src/v1/unsigned.rs @@ -10,9 +10,13 @@ use tari_engine_types::{ substate::SubstateId, }; use tari_ootle_common_types::{Epoch, Signable, SubstateRequirement}; -use tari_template_lib::{models::ComponentAddress, prelude::RistrettoPublicKeyBytes}; +use tari_template_lib::{ + constants::XTR, + models::{ComponentAddress, UtxoAddress}, + prelude::RistrettoPublicKeyBytes, +}; -use crate::{builder::TransactionBuilder, ComponentCall, Instruction, TransactionSignature}; +use crate::{builder::TransactionBuilder, ComponentCall, Instruction, ResourceAddressRef, TransactionSignature}; #[derive(Debug, Clone, Serialize, Deserialize, Default, borsh::BorshSerialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] @@ -134,6 +138,32 @@ impl UnsignedTransactionV1 { Instruction::ClaimValidatorFees { address, .. } => { substates.insert(SubstateId::ValidatorFeePool(*address)); }, + Instruction::StealthTransfer { + resource_address_ref: ResourceAddressRef::Address(addr), + statement, + .. + } => { + substates.insert(SubstateId::Resource(*addr)); + substates.extend( + statement + .inputs_statement + .inputs + .iter() + .map(|i| UtxoAddress::new(*addr, i.commitment.into())) + .map(SubstateId::Utxo), + ); + }, + Instruction::PayFee { statement, .. } => { + substates.insert(SubstateId::Resource(XTR)); + substates.extend( + statement + .inputs_statement + .inputs + .iter() + .map(|i| UtxoAddress::new(XTR, i.commitment.into())) + .map(SubstateId::Utxo), + ); + }, _ => {}, } } diff --git a/crates/wallet/crypto/src/stealth.rs b/crates/wallet/crypto/src/stealth.rs index 435aa72eac..e4ef3ef5b2 100644 --- a/crates/wallet/crypto/src/stealth.rs +++ b/crates/wallet/crypto/src/stealth.rs @@ -34,8 +34,8 @@ pub fn create_transfer_statement<'a, Inputs, Outputs>( required_signer: RistrettoPublicKeyBytes, ) -> Result where - Inputs: IntoIterator, - Outputs: IntoIterator + Clone, + Inputs: IntoIterator + ExactSizeIterator, + Outputs: IntoIterator + ExactSizeIterator + Clone, { if revealed_input_amount.is_negative() { return Err(WalletCryptoError::InvalidArgument { @@ -50,17 +50,14 @@ where }); } - let mut input_iter = inputs.into_iter(); - let num_inputs_estimate = { - let (lower, upper) = input_iter.size_hint(); - upper.unwrap_or(lower) - }; + let num_inputs = inputs.len(); + let num_outputs = output_statements.len(); let outputs_statement = create_outputs_statement(output_statements.clone(), revealed_output_amount)?; let outputs_statement_hash = messages::stealth_statement_metadata64(&outputs_statement); - let (inputs_to_spend, agg_input_mask) = input_iter.try_fold( - (Vec::with_capacity(num_inputs_estimate), RistrettoSecretKey::default()), + let (inputs_to_spend, agg_input_mask) = inputs.into_iter().try_fold( + (Vec::with_capacity(num_inputs), RistrettoSecretKey::default()), |(mut inputs, agg_input), input| { let commitment = input @@ -97,12 +94,16 @@ where required_signer, }; - let balance_proof = generate_stealth_balance_proof_signature( - &agg_input_mask, - &agg_output_mask, - &inputs_statement, - &outputs_statement, - ); + let balance_proof = if num_outputs == 0 && num_inputs == 0 { + None + } else { + Some(generate_stealth_balance_proof_signature( + &agg_input_mask, + &agg_output_mask, + &inputs_statement, + &outputs_statement, + )) + }; Ok(StealthTransferStatement { inputs_statement: StealthInputsStatement { diff --git a/crates/wallet/crypto/src/value_lookup/header.rs b/crates/wallet/crypto/src/value_lookup/header.rs index 524806954d..5754bb453f 100644 --- a/crates/wallet/crypto/src/value_lookup/header.rs +++ b/crates/wallet/crypto/src/value_lookup/header.rs @@ -33,10 +33,10 @@ impl LookupHeader { } let mut u64_buf = [0u8; 8]; u64_buf.copy_from_slice(body.get(..8).expect("slice length checked before")); - let min = u64::from_le_bytes(u64_buf); + let min = u64::from_be_bytes(u64_buf); let mut u64_buf = [0u8; 8]; u64_buf.copy_from_slice(body.get(8..16).expect("slice length checked before")); - let max = u64::from_le_bytes(u64_buf); + let max = u64::from_be_bytes(u64_buf); Ok(Self { min, max }) } diff --git a/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs b/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs index 9b13869fab..b02611446f 100644 --- a/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs +++ b/crates/wallet/crypto/src/value_lookup/io_reader_value_lookup.rs @@ -35,8 +35,11 @@ impl<'a, R: Read + Seek> IoReaderValueLookup<'a, R> { } fn seek_and_buffer_to_value(&mut self, value: u64) -> io::Result<()> { + // Seek to the position of the value. Value must be in range. + assert!(self.header.is_in_range(value)); + let offset_val = value - self.header.min; self.reader - .seek(io::SeekFrom::Start(value * 32 + LookupHeader::SIZE as u64))?; + .seek(io::SeekFrom::Start(offset_val * 32 + LookupHeader::SIZE as u64))?; self.buffer_next()?; Ok(()) } @@ -110,8 +113,8 @@ mod tests { fn generate_lookup_data(min: u64, max: u64) -> Vec { let mut data = Vec::with_capacity(LOOKAHEAD_BUFFER_SIZE + 32 * (max - min + 1) as usize); data.extend_from_slice(LOOKUP_HEADER_LEADING_BYTES); - data.extend_from_slice(&min.to_le_bytes()); - data.extend_from_slice(&max.to_le_bytes()); + data.extend_from_slice(&min.to_be_bytes()); + data.extend_from_slice(&max.to_be_bytes()); for i in min..=max { let byte = i % u64::from(u8::MAX); data.extend_from_slice(&[byte as u8; 32]); @@ -127,6 +130,21 @@ mod tests { assert_eq!(lookup.range(), 0..=10); } + #[test] + fn it_reads_from_the_data_from_offset_start() { + const START: u64 = 1024 * 1024; + const END: u64 = 2 * 1024 * 1024; + let lookup_data = generate_lookup_data(START, END); + let mut reader = Cursor::new(lookup_data.as_slice()); + let mut lookup = IoReaderValueLookup::load(&mut reader).unwrap(); + assert!(lookup.lookup(START - 1).unwrap().is_none()); + for v in START..=END { + let value = lookup.lookup(v).unwrap().unwrap(); + let byte = v % u64::from(u8::MAX); + assert_eq!(value, [byte as u8; 32], "Failed at value {}", v); + } + } + #[test] fn it_reads_from_the_data_file_until_end() { const NUM: u64 = (LOOKAHEAD_BUFFER_SIZE + 11) as u64; diff --git a/crates/wallet/crypto/src/value_lookup/mod.rs b/crates/wallet/crypto/src/value_lookup/mod.rs index e4bf8ce040..29d34ac227 100644 --- a/crates/wallet/crypto/src/value_lookup/mod.rs +++ b/crates/wallet/crypto/src/value_lookup/mod.rs @@ -8,7 +8,7 @@ mod io_reader_value_lookup; use std::convert::Infallible; pub use io_reader_value_lookup::*; -use tari_engine_types::crypto::ValueLookupTable; +pub use tari_engine_types::crypto::ValueLookupTable; #[derive(Clone)] pub struct AlwaysMissLookupTable; diff --git a/crates/wallet/crypto/tests/stealth_transfer_statement.rs b/crates/wallet/crypto/tests/stealth_transfer_statement.rs index dc1bb70bfc..4e81e087b5 100644 --- a/crates/wallet/crypto/tests/stealth_transfer_statement.rs +++ b/crates/wallet/crypto/tests/stealth_transfer_statement.rs @@ -1,6 +1,8 @@ // Copyright 2024 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use std::iter; + use chacha20poly1305::aead::OsRng; use tari_crypto::{ keys::{PublicKey, SecretKey}, @@ -44,9 +46,9 @@ mod stealth_tests { #[test] fn it_errors_for_noop_transfer() { let statement = create_transfer_statement( - &[], + iter::empty(), Amount::zero(), - &[], + iter::empty(), Amount::zero(), RistrettoPublicKeyBytes::zero(), ) @@ -63,9 +65,9 @@ mod stealth_tests { let revealed_output_amount = Amount::from(0); let statement = create_transfer_statement( - &inputs, + inputs.iter(), revealed_input_amount, - &output_statements, + output_statements.iter(), revealed_output_amount, RistrettoPublicKeyBytes::zero(), ) @@ -83,9 +85,9 @@ mod stealth_tests { let revealed_output_amount = Amount::from(6000 + 6000 - 100 - 200 - 300); let statement = create_transfer_statement( - &inputs, + inputs.iter(), revealed_input_amount, - &output_statements, + output_statements.iter(), revealed_output_amount, RistrettoPublicKeyBytes::zero(), ) @@ -99,9 +101,9 @@ mod stealth_tests { let revealed_input_amount = Amount::from(6000); let revealed_output_amount = Amount::from(6000); let statement = create_transfer_statement( - &[], + iter::empty(), revealed_input_amount, - &[], + iter::empty(), revealed_output_amount, RistrettoPublicKeyBytes::zero(), ) @@ -111,9 +113,9 @@ mod stealth_tests { let revealed_input_amount = Amount::from(6000); let revealed_output_amount = Amount::from(5999); let statement = create_transfer_statement( - &[], + iter::empty(), revealed_input_amount, - &[], + iter::empty(), revealed_output_amount, RistrettoPublicKeyBytes::zero(), ) @@ -131,9 +133,9 @@ mod stealth_tests { let revealed_output_amount = Amount::from(6000 + 6000 - 100 - 200 - 300); let mut statement = create_transfer_statement( - &inputs, + inputs.iter(), revealed_input_amount, - &output_statements, + output_statements.iter(), revealed_output_amount, required_signer, ) @@ -153,14 +155,15 @@ mod stealth_tests { .map(|stmt| &stmt.mask_and_value.mask) .fold(RistrettoSecretKey::default(), |agg, mask| agg + mask); - statement.outputs_statement = create_outputs_statement(&output_statements, revealed_output_amount).unwrap(); + statement.outputs_statement = + create_outputs_statement(output_statements.iter(), revealed_output_amount).unwrap(); - statement.balance_proof = generate_stealth_balance_proof_signature( + statement.balance_proof = Some(generate_stealth_balance_proof_signature( &agg_input_mask, &agg_output_mask, &statement.inputs_statement, &statement.outputs_statement, - ); + )); // This passes because we recreated the balance proof correctly stealth::validate_transfer_balance(&statement, None).unwrap(); diff --git a/crates/wallet/sdk/src/apis/confidential_outputs.rs b/crates/wallet/sdk/src/apis/confidential_outputs.rs index 18734e7828..bb9f647214 100644 --- a/crates/wallet/sdk/src/apis/confidential_outputs.rs +++ b/crates/wallet/sdk/src/apis/confidential_outputs.rs @@ -7,7 +7,6 @@ use tari_engine_types::{crypto::PrivateOutput, ConvertFromByteType, ToByteType}; use tari_ootle_common_types::optional::{IsNotFoundError, Optional}; use tari_ootle_wallet_crypto::{kdfs, MaskAndValue}; use tari_template_lib::{models::VaultId, prelude::PedersenCommitmentBytes, types::Amount}; -use tari_transaction::TransactionId; use crate::{ apis::{ @@ -321,16 +320,6 @@ where TStore: WalletStore lock_id: None, }) } - - pub fn locks_set_transaction_id( - &self, - lock_id: WalletLockId, - transaction_id: TransactionId, - ) -> Result<(), ConfidentialOutputsApiError> { - self.store - .with_write_tx(|tx| tx.locks_link_transaction(lock_id, transaction_id))?; - Ok(()) - } } #[derive(Debug, thiserror::Error)] diff --git a/crates/wallet/sdk/src/apis/confidential_transfer.rs b/crates/wallet/sdk/src/apis/confidential_transfer.rs index f3f453da5b..e11dada28e 100644 --- a/crates/wallet/sdk/src/apis/confidential_transfer.rs +++ b/crates/wallet/sdk/src/apis/confidential_transfer.rs @@ -25,9 +25,10 @@ use crate::{ config::{ConfigApi, ConfigApiError}, key_manager::{KeyManagerApi, KeyManagerApiError}, substate::{SubstateApiError, SubstatesApi}, + transaction::{TransactionApi, TransactionApiError}, }, models::{ConfidentialOutputModel, KeyBranch, OutputStatus, WalletLockId}, - network::WalletNetworkInterface, + network::{StatusResponseError, WalletNetworkInterface}, storage::{WalletStorageError, WalletStore}, }; @@ -37,6 +38,7 @@ pub struct ConfidentialTransferApi<'a, TStore, TNetworkInterface> { key_manager_api: KeyManagerApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, confidential_outputs_api: ConfidentialOutputsApi<'a, TStore>, + transaction_api: TransactionApi<'a, TStore, TNetworkInterface>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, crypto_api: ConfidentialCryptoApi, config_api: ConfigApi<'a, TStore>, @@ -46,13 +48,14 @@ impl<'a, TStore, TNetworkInterface> ConfidentialTransferApi<'a, TStore, TNetwork where TStore: WalletStore, TNetworkInterface: WalletNetworkInterface, - TNetworkInterface::Error: IsNotFoundError, + TNetworkInterface::Error: IsNotFoundError + StatusResponseError, { pub fn new( key_manager_api: KeyManagerApi<'a, TStore>, accounts_api: AccountsApi<'a, TStore, TNetworkInterface>, confidential_outputs_api: ConfidentialOutputsApi<'a, TStore>, substate_api: SubstatesApi<'a, TStore, TNetworkInterface>, + transaction_api: TransactionApi<'a, TStore, TNetworkInterface>, crypto_api: ConfidentialCryptoApi, config_api: ConfigApi<'a, TStore>, ) -> Self { @@ -61,6 +64,7 @@ where accounts_api, confidential_outputs_api, substate_api, + transaction_api, crypto_api, config_api, } @@ -73,7 +77,7 @@ where from_account: ComponentAddress, resource_address: ResourceAddress, spend_amount: Amount, - input_selection: ConfidentialTransferInputSelection, + input_selection: UtxoInputSelection, ) -> Result { let src_vault = self .accounts_api @@ -82,7 +86,7 @@ where let available_revealed_funds = src_vault.available_revealed_balance(); match &input_selection { - ConfidentialTransferInputSelection::ConfidentialOnly => { + UtxoInputSelection::ConfidentialOnly => { let (confidential_inputs, _) = self.confidential_outputs_api .lock_outputs_by_amount(lock_id, &src_vault.id, spend_amount)?; @@ -103,7 +107,7 @@ where revealed: Amount::zero(), }) }, - ConfidentialTransferInputSelection::RevealedOnly => { + UtxoInputSelection::RevealedOnly => { if available_revealed_funds < spend_amount { return Err(ConfidentialTransferApiError::InsufficientFunds); } @@ -124,7 +128,7 @@ where revealed: spend_amount, }) }, - ConfidentialTransferInputSelection::PreferRevealed => { + UtxoInputSelection::PreferRevealed => { let revealed_to_spend = cmp::min(src_vault.revealed_balance, spend_amount); let confidential_to_spend = spend_amount - revealed_to_spend; if confidential_to_spend.is_zero() { @@ -179,7 +183,7 @@ where revealed: revealed_to_spend, }) }, - ConfidentialTransferInputSelection::PreferConfidential => { + UtxoInputSelection::PreferConfidential => { let (confidential_inputs, amount_locked) = self .confidential_outputs_api .lock_outputs_until_partial_amount(lock_id, &src_vault.id, spend_amount)?; @@ -416,7 +420,7 @@ where .build_and_seal(&account_key.secret); let tx_id = transaction.calculate_id(); - self.confidential_outputs_api + self.transaction_api .locks_set_transaction_id(inputs_to_spend.lock_id, tx_id)?; Ok(TransferOutput { @@ -478,7 +482,7 @@ pub struct ConfidentialTransferParams { /// Spend from this account pub from_account: ComponentAddress, /// Strategy for input selection - pub input_selection: ConfidentialTransferInputSelection, + pub input_selection: UtxoInputSelection, /// Amount to spend to destination pub amount: Amount, /// Destination address used to derive the destination account component @@ -524,7 +528,7 @@ impl ConfidentialTransferParams { #[derive(Debug, Clone, Copy, Deserialize, Serialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export))] -pub enum ConfidentialTransferInputSelection { +pub enum UtxoInputSelection { ConfidentialOnly, RevealedOnly, PreferRevealed, @@ -572,6 +576,8 @@ pub enum ConfidentialTransferApiError { ConfigApi(#[from] ConfigApiError), #[error("Amount overflow for parameter `{param}`: {details}")] AmountOverflow { param: &'static str, details: String }, + #[error("Transaction API error: {0}")] + TransactionApiError(#[from] TransactionApiError), } impl IsNotFoundError for ConfidentialTransferApiError { diff --git a/crates/wallet/sdk/src/apis/stealth_crypto.rs b/crates/wallet/sdk/src/apis/stealth_crypto.rs index 54b524d270..ae80139840 100644 --- a/crates/wallet/sdk/src/apis/stealth_crypto.rs +++ b/crates/wallet/sdk/src/apis/stealth_crypto.rs @@ -55,8 +55,8 @@ impl StealthCryptoApi { ) -> Result where A: Into, - Inputs: IntoIterator, - Outputs: IntoIterator + Clone, + Inputs: IntoIterator + ExactSizeIterator, + Outputs: IntoIterator + ExactSizeIterator + Clone, { let stmt = stealth::create_transfer_statement( inputs, diff --git a/crates/wallet/sdk/src/apis/stealth_outputs.rs b/crates/wallet/sdk/src/apis/stealth_outputs.rs index acf7d82af8..81dd1b4fb3 100644 --- a/crates/wallet/sdk/src/apis/stealth_outputs.rs +++ b/crates/wallet/sdk/src/apis/stealth_outputs.rs @@ -94,7 +94,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { .non_negative_checked() .ok_or_else(|| StealthOutputsApiError::InvalidParameter { param: "amount", - reason: "Amount must be non-negative".to_string(), + reason: "lock_outputs_for_at_least_amount: Amount must be non-negative".to_string(), })?; self.store.with_write_tx(|tx| { let (outputs, total_output_amount) = @@ -132,7 +132,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { if amount.is_negative() { return Err(StealthOutputsApiError::InvalidParameter { param: "amount", - reason: "Amount cannot be negative".to_string(), + reason: "lock_outputs_internal: Amount cannot be negative".to_string(), }); } let mut total_output_amount = Amount::zero(); @@ -179,6 +179,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { Ok(()) } + // TODO: move into a lock api pub fn create_lock(&self) -> Result { let lock_id = self.store.with_write_tx(|tx| tx.locks_create())?; Ok(lock_id) @@ -607,7 +608,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { if !amount.is_positive() { return Err(StealthOutputsApiError::InvalidParameter { param: "amount", - reason: format!("Amount must be positive, got {}", amount), + reason: format!("create_output_witness: Amount must be positive, got {}", amount), }); } @@ -702,7 +703,7 @@ impl<'a, TStore: WalletStore> StealthOutputsApi<'a, TStore> { let statement = self.crypto_api.generate_transfer_statement( unblinded_inputs.iter().map(|i| &i.witness), params.input_revealed_amount, - &outputs, + outputs.iter(), params.output_revealed_amount, params.required_signer, )?; diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index d4eb264eb6..7cd3827fbc 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -39,7 +39,7 @@ use super::{ use crate::{ apis::{ accounts::{derive_account_address_from_public_key, AccountsApi}, - confidential_transfer::ConfidentialTransferInputSelection, + confidential_transfer::UtxoInputSelection, config::ConfigApi, key_manager::KeyManagerApi, stealth_outputs::{StealthOutputsApi, TransferStatementParams}, @@ -91,7 +91,7 @@ where owner_account_component_address: &ComponentAddress, resource_address: ResourceAddress, spend_amount: Amount, - input_selection: ConfidentialTransferInputSelection, + input_selection: UtxoInputSelection, ) -> Result { if !spend_amount.is_positive() { return Err(StealthTransferApiError::InvalidParameter { @@ -111,7 +111,7 @@ where .unwrap_or_else(Amount::zero); match input_selection { - ConfidentialTransferInputSelection::ConfidentialOnly => { + UtxoInputSelection::ConfidentialOnly => { let (inputs, total_locked) = self.outputs_api.lock_outputs_for_at_least_amount( owner_account_component_address, &resource_address, @@ -131,9 +131,13 @@ where revealed: Amount::zero(), }) }, - ConfidentialTransferInputSelection::RevealedOnly => { + UtxoInputSelection::RevealedOnly => { if available_revealed_funds < spend_amount { - return Err(StealthTransferApiError::InsufficientFunds); + return Err(StealthTransferApiError::InsufficientFunds { + details: "RevealedOnly: Not enough revealed funds to spend.".to_string(), + available: available_revealed_funds, + required: spend_amount, + }); } let src_vault = @@ -161,7 +165,7 @@ where revealed: spend_amount, }) }, - ConfidentialTransferInputSelection::PreferRevealed => { + UtxoInputSelection::PreferRevealed => { let revealed_to_spend = cmp::min(available_revealed_funds, spend_amount); let utxo_amount_to_spend = spend_amount - revealed_to_spend; if let Some(ref src_vault) = maybe_src_vault { @@ -227,7 +231,7 @@ where revealed: revealed_to_spend, }) }, - ConfidentialTransferInputSelection::PreferConfidential => { + UtxoInputSelection::PreferConfidential => { let lock_id = self.outputs_api.create_lock()?; let (inputs, blinded_amount_locked) = self.outputs_api.lock_outputs_until_partial_amount( owner_account_component_address, @@ -241,7 +245,11 @@ where .unwrap_or_else(Amount::zero); if available_revealed_funds < revealed_to_spend { - return Err(StealthTransferApiError::InsufficientFunds); + return Err(StealthTransferApiError::InsufficientFunds { + details: "PreferConfidential: Not enough revealed funds to spend.".to_string(), + available: available_revealed_funds, + required: revealed_to_spend, + }); } if revealed_to_spend.is_positive() { @@ -273,7 +281,7 @@ where lock_id: WalletLockId, owner_account: &AccountWithAddress, max_fee: A, - input_selection: ConfidentialTransferInputSelection, + input_selection: UtxoInputSelection, ) -> Result { self.lock_inputs_for_transfer( lock_id, @@ -350,7 +358,7 @@ where // Lock up funds for fees and transfer let fee_inputs_to_spend = self.unlock_on_failure( lock_id, - self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.input_selection), + self.lock_fee_inputs(lock_id, &owner_account, params.max_fee, params.fee_input_selection), )?; let fee_stealth_change_amt = fee_inputs_to_spend diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs index 611c25e11e..8405f08923 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/error.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/error.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_common_types::optional::IsNotFoundError; -use tari_template_lib::models::ResourceAddress; +use tari_template_lib::{models::ResourceAddress, types::Amount}; use crate::{ apis::{ @@ -28,8 +28,12 @@ pub enum StealthTransferApiError { SubstateApi(#[from] SubstateApiError), #[error("Key manager error: {0}")] KeyManagerApi(#[from] KeyManagerApiError), - #[error("Insufficient funds")] - InsufficientFunds, + #[error("Insufficient funds: {details}. Available: {available}, Required: {required}")] + InsufficientFunds { + details: String, + available: Amount, + required: Amount, + }, #[error("Badge vault not found for resource {resource_address}")] BadgeVaultNotFound { resource_address: ResourceAddress }, #[error("Accounts API error: {0}")] diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs index f0c2e3e11f..9de2f0506f 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs @@ -12,14 +12,16 @@ use tari_template_lib::{ }; use crate::apis::{ - confidential_transfer::ConfidentialTransferInputSelection, + confidential_transfer::UtxoInputSelection, stealth_transfer::{StealthOutputToCreate, StealthTransferApiError}, }; #[derive(Debug)] pub struct StealthTransferParams { /// Strategy for input selection - pub input_selection: ConfidentialTransferInputSelection, + pub fee_input_selection: UtxoInputSelection, + /// Strategy for input selection + pub input_selection: UtxoInputSelection, pub outputs: Vec, pub badge_usage: BadgeUsage, /// Address of the resource to transfer @@ -104,7 +106,8 @@ impl StealthTransferParams { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub struct TransferOutput { /// Destination address used to derive the UTXO encryption keys, owner signature and the account in which to /// deposit revealed funds diff --git a/crates/wallet/sdk/src/apis/substate.rs b/crates/wallet/sdk/src/apis/substate.rs index f6afb6d678..4c54b4cf32 100644 --- a/crates/wallet/sdk/src/apis/substate.rs +++ b/crates/wallet/sdk/src/apis/substate.rs @@ -72,6 +72,10 @@ where &self, ids: Vec, ) -> Result, SubstateApiError> { + if ids.is_empty() { + return Ok(HashMap::new()); + } + self.network_interface .get_substates(ids) .await diff --git a/crates/wallet/sdk/src/models/account.rs b/crates/wallet/sdk/src/models/account.rs index 2037b3aaed..a3195e07d4 100644 --- a/crates/wallet/sdk/src/models/account.rs +++ b/crates/wallet/sdk/src/models/account.rs @@ -68,6 +68,10 @@ pub struct AccountWithAddress { } impl AccountWithAddress { + pub fn new(account: Account, address: OotleAddress) -> Self { + Self { account, address } + } + pub fn account(&self) -> &Account { &self.account } diff --git a/crates/wallet/sdk/src/models/key.rs b/crates/wallet/sdk/src/models/key.rs index 6c5ff07a29..9d1f2e9f21 100644 --- a/crates/wallet/sdk/src/models/key.rs +++ b/crates/wallet/sdk/src/models/key.rs @@ -283,6 +283,32 @@ impl From for KeyIdOrPublicKey { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] +pub struct BranchAndKeyId { + pub branch: KeyBranch, + pub key_id: KeyId, +} + +impl BranchAndKeyId { + pub fn new(branch: KeyBranch, key_id: KeyId) -> Self { + Self { branch, key_id } + } + + pub fn for_account(key_id: KeyId) -> Self { + Self { + branch: KeyBranch::Account, + key_id, + } + } +} + +impl Display for BranchAndKeyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.branch.as_str(), self.key_id) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "wallet-daemon-client/"))] pub enum KeyId { diff --git a/crates/wallet/sdk/src/models/lock_guard.rs b/crates/wallet/sdk/src/models/lock_guard.rs new file mode 100644 index 0000000000..cc1601622f --- /dev/null +++ b/crates/wallet/sdk/src/models/lock_guard.rs @@ -0,0 +1,62 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use crate::{ + models::WalletLockId, + storage::{CommitableStore, WalletStoreWriter, WriteableWalletStore}, +}; + +const LOG_TARGET: &str = "tari::ootle::wallet::models::lock_guard"; + +pub struct WalletLockDropGuard { + lock_id: WalletLockId, + store: Option, +} + +impl WalletLockDropGuard +where TStore: WriteableWalletStore +{ + pub fn new(lock_id: WalletLockId, store: TStore) -> Self { + Self { + lock_id, + store: Some(store), + } + } + + pub fn lock_id(&self) -> &WalletLockId { + &self.lock_id + } + + pub fn disarm(mut self) { + self.store = None; + } +} + +impl Drop for WalletLockDropGuard +where TStore: WriteableWalletStore +{ + fn drop(&mut self) { + let Some(store) = self.store.take() else { + // Lock guard disarmed, do nothing + return; + }; + match store.create_write_tx() { + Ok(mut tx) => { + if let Err(e) = tx.locks_release(self.lock_id) { + log::error!(target: LOG_TARGET, "Failed to unlock wallet lock {:?}: {}", self.lock_id, e); + } + if let Err(e) = tx.commit() { + log::error!(target: LOG_TARGET, "Failed to commit unlock for wallet lock {:?}: {}", self.lock_id, e); + } + }, + Err(e) => { + log::error!( + target: LOG_TARGET, + "Failed to create write transaction to unlock wallet lock {:?}: {}", + self.lock_id, + e + ); + }, + }; + } +} diff --git a/crates/wallet/sdk/src/models/mod.rs b/crates/wallet/sdk/src/models/mod.rs index 25f730370c..49886040f3 100644 --- a/crates/wallet/sdk/src/models/mod.rs +++ b/crates/wallet/sdk/src/models/mod.rs @@ -6,6 +6,7 @@ mod authored_template; mod confidential_output; mod config; mod key; +mod lock_guard; mod non_fungible_tokens; mod resource; mod stealth_output; @@ -20,6 +21,7 @@ pub use authored_template::*; pub use confidential_output::*; pub use config::Config; pub use key::*; +pub use lock_guard::*; pub use non_fungible_tokens::*; pub use resource::*; pub use stealth_output::*; diff --git a/crates/wallet/sdk/src/models/wallet_transaction.rs b/crates/wallet/sdk/src/models/wallet_transaction.rs index 334fe35ad0..0c685b3855 100644 --- a/crates/wallet/sdk/src/models/wallet_transaction.rs +++ b/crates/wallet/sdk/src/models/wallet_transaction.rs @@ -48,6 +48,14 @@ pub enum TransactionStatus { } impl TransactionStatus { + pub fn is_any_accept(&self) -> bool { + matches!(self, TransactionStatus::Accepted | TransactionStatus::OnlyFeeAccepted) + } + + pub fn is_accepted(&self) -> bool { + matches!(self, TransactionStatus::Accepted) + } + pub fn as_key_str(&self) -> &'static str { match self { TransactionStatus::New => "New", diff --git a/crates/wallet/sdk/src/sdk.rs b/crates/wallet/sdk/src/sdk.rs index 15bce85617..8490f6a529 100644 --- a/crates/wallet/sdk/src/sdk.rs +++ b/crates/wallet/sdk/src/sdk.rs @@ -252,6 +252,7 @@ where self.accounts_api(), self.confidential_outputs_api(), self.substate_api(), + self.transaction_api(), self.confidential_crypto_api(), self.config_api(), ) diff --git a/crates/wallet/sdk_services/src/indexer_rest_api.rs b/crates/wallet/sdk_services/src/indexer_rest_api.rs index d9f5f2eb1f..87c2d71099 100644 --- a/crates/wallet/sdk_services/src/indexer_rest_api.rs +++ b/crates/wallet/sdk_services/src/indexer_rest_api.rs @@ -20,8 +20,8 @@ use tari_indexer_client::{ types::{ GetSubstatesRequest, GetTransactionResultRequest, - GetUnspentUtxosRequest, GetUtxoUpdatesRequest, + GetUtxosRequest, IndexerTransactionFinalizedResult, SubmitTransactionRequest, }, @@ -255,7 +255,7 @@ impl WalletNetworkInterface for IndexerRestApiNetworkInterface { // TODO: Given the potential size of substates protobuf, json + hex encoding may be too inefficient. Consider // supporting the application/x-protobuf content type in the indexer REST API. let resp = client - .get_unspent_utxos(GetUnspentUtxosRequest { + .get_utxos(GetUtxosRequest { resource_address, tag_and_nonce_pairs, }) diff --git a/integration_tests/src/wallet_daemon_client.rs b/integration_tests/src/wallet_daemon_client.rs index 4c2d350a16..2655aa51cd 100644 --- a/integration_tests/src/wallet_daemon_client.rs +++ b/integration_tests/src/wallet_daemon_client.rs @@ -28,8 +28,8 @@ use tari_engine_types::substate::SubstateId; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_wallet_sdk::{ - apis::{confidential_transfer::ConfidentialTransferInputSelection, stealth_transfer::BadgeUsage}, - models::{Account, AccountWithAddress, NonFungibleToken}, + apis::{confidential_transfer::UtxoInputSelection, stealth_transfer::BadgeUsage}, + models::{Account, AccountWithAddress, BranchAndKeyId, KeyBranch, NonFungibleToken}, }; use tari_template_lib::{ prelude::{ResourceAddress, RistrettoPublicKeyBytes}, @@ -121,7 +121,8 @@ pub async fn transfer_stealth( let resp = client .accounts_stealth_transfer(StealthTransferRequest { owner_account: source_account_name, - input_selection: ConfidentialTransferInputSelection::PreferRevealed, + fee_input_selection: UtxoInputSelection::PreferRevealed, + input_selection: UtxoInputSelection::PreferRevealed, resource_address, badge_usage: BadgeUsage::None, transfers: vec![StealthTransfer { @@ -437,6 +438,7 @@ pub async fn submit_manifest_with_signing_keys( let account_name = ComponentAddressOrName::Name(account_signing_key); let AccountGetResponse { account, .. } = client.accounts_get(account_name).await.unwrap(); + let owner_key_id = account.owner_key_id.expect("Account has no owner key id for signing"); let instructions = parse_manifest(&manifest_content, globals, HashMap::new()).unwrap(); @@ -450,10 +452,11 @@ pub async fn submit_manifest_with_signing_keys( let transaction_submit_req = TransactionSubmitRequest { transaction, - signing_key_id: account.owner_key_id, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: true, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = client.submit_transaction(transaction_submit_req).await.unwrap(); @@ -525,6 +528,8 @@ pub async fn submit_manifest( let AccountGetResponse { account, .. } = client.accounts_get_default().await.unwrap(); + let owner_key_id = account.owner_key_id.expect("Account has no owner key id for signing"); + let transaction = transaction_builder() .fee_transaction_pay_from_component(account.component_address, 5000) .with_instructions(instructions.instructions) @@ -535,10 +540,11 @@ pub async fn submit_manifest( let transaction_submit_req = TransactionSubmitRequest { transaction, - signing_key_id: account.owner_key_id, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: true, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = client.submit_transaction(transaction_submit_req).await.unwrap(); @@ -641,6 +647,8 @@ pub async fn create_component( .await .unwrap(); + let owner_key_id = account.owner_key_id.expect("Account has no owner key id for signing"); + let transaction = transaction_builder() .fee_transaction_pay_from_component(account.component_address, 5000) .call_function(template_address, &function_call, args) @@ -650,10 +658,11 @@ pub async fn create_component( let transaction_submit_req = TransactionSubmitRequest { transaction, - signing_key_id: account.owner_key_id, + seal_signer: BranchAndKeyId::for_account(owner_key_id), + other_signers: vec![], detect_inputs: true, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let resp = client.submit_transaction(transaction_submit_req).await.unwrap(); @@ -888,7 +897,7 @@ pub async fn confidential_transfer( proof_from_badge_resource: None, memo: None, dry_run: false, - input_selection: ConfidentialTransferInputSelection::PreferRevealed, + input_selection: UtxoInputSelection::PreferRevealed, output_to_revealed: false, }; @@ -940,10 +949,11 @@ async fn submit_unsigned_tx_and_wait_for_response( ); let submit_req = TransactionSubmitRequest { transaction, - signing_key_id: account.owner_key_id, + seal_signer: BranchAndKeyId::new(KeyBranch::Account, account.owner_key_id.expect("no owner_key_id")), + other_signers: vec![], detect_inputs: true, detect_inputs_use_unversioned: use_unversioned_inputs, - proof_ids: vec![], + lock_ids: vec![], }; let submit_resp = client.submit_transaction(submit_req).await?; diff --git a/integration_tests/tests/steps/wallet_daemon.rs b/integration_tests/tests/steps/wallet_daemon.rs index da24d99819..a2d292b968 100644 --- a/integration_tests/tests/steps/wallet_daemon.rs +++ b/integration_tests/tests/steps/wallet_daemon.rs @@ -12,7 +12,7 @@ use integration_tests::{ }; use rand::{rngs::OsRng, Rng}; use tari_engine_types::commit_result::FinalizeResult; -use tari_ootle_wallet_sdk::models::KeyBranch; +use tari_ootle_wallet_sdk::models::{BranchAndKeyId, KeyBranch}; use tari_template_lib::{ constants::XTR, types::{bytes::Bytes, crypto::PedersenCommitmentBytes, Amount}, @@ -127,10 +127,11 @@ async fn when_i_run_up_fees(world: &mut TariWorld, amount: u64, wallet_daemon_na let transaction_submit_req = TransactionSubmitRequest { transaction, - signing_key_id: account.owner_key_id(), + seal_signer: BranchAndKeyId::new(KeyBranch::Account, account.owner_key_id().expect("no owner key id")), + other_signers: vec![], detect_inputs: true, detect_inputs_use_unversioned: true, - proof_ids: vec![], + lock_ids: vec![], }; let walletd = world.get_wallet_daemon(&wallet_daemon_name); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d4a829582..83c7d660a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -392,7 +392,7 @@ importers: version: 19.1.2(@types/react@19.1.0) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.3.6(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) + version: 4.3.4(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1)) eslint: specifier: ^9.21.0 version: 9.24.0(jiti@2.4.2) @@ -412,8 +412,8 @@ importers: specifier: ^8.24.1 version: 8.29.1(eslint@9.24.0(jiti@2.4.2))(typescript@5.7.3) vite: - specifier: ^6.3.6 - version: 6.3.6(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) + specifier: ^6.4.1 + version: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) packages: @@ -7848,17 +7848,6 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.3.4(vite@6.3.6(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1))': - dependencies: - '@babel/core': 7.26.10 - '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) - '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 6.3.6(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.3.4(vite@6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@babel/core': 7.26.10 @@ -11230,7 +11219,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.6(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) + vite: 6.4.1(@types/node@22.14.0)(jiti@2.4.2)(lightningcss@1.30.1) transitivePeerDependencies: - '@types/node' - jiti diff --git a/utilities/generate_ristretto_value_lookup/Cargo.toml b/utilities/generate_ristretto_value_lookup/Cargo.toml index 918ed51bb6..766861d9f7 100644 --- a/utilities/generate_ristretto_value_lookup/Cargo.toml +++ b/utilities/generate_ristretto_value_lookup/Cargo.toml @@ -8,7 +8,10 @@ license.workspace = true [dependencies] tari_crypto = { workspace = true } +tari_ootle_wallet_crypto = { workspace = true } clap = { workspace = true, features = ["derive"] } human_bytes = { version = "0.4.3", default-features = false } -humantime = { workspace = true } \ No newline at end of file +humantime = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +futures = { workspace = true } \ No newline at end of file diff --git a/utilities/generate_ristretto_value_lookup/src/cli.rs b/utilities/generate_ristretto_value_lookup/src/cli.rs index 4081120772..980663a027 100644 --- a/utilities/generate_ristretto_value_lookup/src/cli.rs +++ b/utilities/generate_ristretto_value_lookup/src/cli.rs @@ -18,6 +18,13 @@ pub struct Cli { /// The maximum value to include in the lookup table #[clap(short = 'x', long)] pub max: u64, + /// The number of worker threads to use + #[clap(short = 'j', long)] + pub jobs: Option, + + /// Validate the lookup table at the path + #[clap(short = 'v', long, help = "Validate the lookup table at the path")] + pub validate: bool, } impl Cli { diff --git a/utilities/generate_ristretto_value_lookup/src/main.rs b/utilities/generate_ristretto_value_lookup/src/main.rs index c3a66a1304..19adf16d99 100644 --- a/utilities/generate_ristretto_value_lookup/src/main.rs +++ b/utilities/generate_ristretto_value_lookup/src/main.rs @@ -8,74 +8,188 @@ use std::{ time::{Duration, Instant}, }; +use futures::{stream::FuturesOrdered, StreamExt}; use human_bytes::human_bytes; use tari_crypto::{ keys::PublicKey, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, tari_utilities::ByteArray, }; +use tari_ootle_wallet_crypto::ValueLookupTable; use crate::cli::Cli; mod cli; -fn main() -> io::Result<()> { +#[tokio::main] +async fn main() -> io::Result<()> { let cli = Cli::init(); let dest_file = cli.output_file; + if cli.validate { + println!( + "Validating Ristretto value lookup table at {}. NOTE: this will probably take hours depending on the \ + value range of the file.", + dest_file.display() + ); + let timer = Instant::now(); + let mut file = fs::File::open(&dest_file)?; + let mut lookup = tari_ootle_wallet_crypto::IoReaderValueLookup::load(&mut file)?; + for v in lookup.range() { + let pk_bytes = lookup.lookup(v)?.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidData, + format!("Value {} not found in lookup table.", v), + ) + })?; + let expected_pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); + if pk_bytes != expected_pk.as_bytes() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Public key mismatch for value {}.", v), + )); + } + } + let elapsed = timer.elapsed(); + println!( + "Validation completed successfully in {}.", + humantime::format_duration(elapsed) + ); + return Ok(()); + } + let file_size = (cli.max - cli.min + 1) * 32 + 20; - // Rough estimate: 200ms per 10,000 values on a "typical" cpu - let est_time = Duration::from_millis((cli.max - cli.min + 1) / 10000 * 200); println!( - "Generating Ristretto value lookup table from {} to {} and writing to {} ({}). This will take roughly {} to \ - complete.", + "Generating Ristretto value lookup table from {} to {} and writing to {} ({}).", cli.min, cli.max, dest_file.display(), human_bytes(file_size as f64), - humantime::format_duration(est_time) ); println!(); let writer = fs::File::create(&dest_file)?; - let timer = Instant::now(); - write_output(writer, cli.min, cli.max)?; - let elapsed = timer.elapsed(); + // Determine number of workers + let jobs = cli + .jobs + .unwrap_or_else(|| tokio::runtime::Handle::current().metrics().num_workers()); + write_output_async(writer, cli.min, cli.max, jobs).await?; println!(); let metadata = fs::metadata(&dest_file)?; println!( - "Output written to {} ({}) in {}", + "Output written to {} ({})", dest_file.display(), human_bytes(metadata.len() as f64), - humantime::format_duration(elapsed) ); Ok(()) } -fn write_output(mut writer: W, min: u64, max: u64) -> io::Result<()> { +async fn write_output_async(mut writer: W, min: u64, max: u64, num_threads: usize) -> io::Result<()> { // Write header VLKP || min_value (8 bytes) || max_value (8 bytes) writer.write_all(b"VLKP")?; writer.write_all(&min.to_be_bytes())?; writer.write_all(&max.to_be_bytes())?; + println!( + "Using {} worker threads to generate Ristretto public keys.", + num_threads + ); + + const CHUNK_SIZE: usize = 10_000; // 320 KB per thread, because of the result ordering, making the chunk size larger actually degrades performance + + let timer = Instant::now(); + + let mut chunks = (min..=max) + .step_by(CHUNK_SIZE) + .map(|chunk_start| { + let chunk_end = std::cmp::min(chunk_start + CHUNK_SIZE as u64 - 1, max); + (chunk_start, chunk_end) + }) + .enumerate(); + + // Pre-allocate scratch pad for each thread + let mut scratch_pad = vec![Some(Vec::with_capacity(CHUNK_SIZE)); num_threads]; + + let mut handles = FuturesOrdered::new(); + let mut count = 0; let mut dot_count = 0; - for v in min..=max { - let p = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); - writer.write_all(p.as_bytes())?; - if v % 10000 == 0 { - dot_count += 1; + let mut line_count = 1; + + loop { + while handles.len() < num_threads { + if let Some((i, (chunk_start, chunk_end))) = chunks.by_ref().next() { + let mut results = scratch_pad[i % num_threads].take().unwrap(); + // Spawn tasks for each chunk + let handle = tokio::task::spawn_blocking(move || { + for v in chunk_start..=chunk_end { + let mut buf = [0u8; 32]; + let pk = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::from(v)); + buf.copy_from_slice(pk.as_bytes()); + results.push(buf); + } + (i % num_threads, results) + }); + + handles.push_back(handle); + } else { + break; + } + } + + if handles.is_empty() { + break; + } + + let (i, mut results) = handles.next().await.expect("handles stream end")?; + for pk_bytes in &results { + count += 1; + writer.write_all(pk_bytes)?; + } + results.clear(); + scratch_pad[i].replace(results); + + if count % 10000 == 0 { print!("."); + dot_count += 1; stdout().flush()?; } + if dot_count == 80 { dot_count = 0; + line_count += 1; println!(); } + + if line_count % 5 == 0 { + line_count = 1; + let completed = count; + let elapsed = timer.elapsed(); + let est_time = Duration::from_secs( + ((max - min + 1 - completed) as f64 / completed as f64 * elapsed.as_secs_f64()).round() as u64, + ); + println!( + "{:.1}% ETA: {}. {}/{} values generated in {}", + (completed as f64 / (max - min + 1) as f64) * 100.0, + humantime::format_duration(est_time), + completed, + max - min, + humantime::format_duration(Duration::from_secs(elapsed.as_secs())) + ); + } } + + println!(); + println!( + "Completed generation of Ristretto value lookup table from {} to {} in {}", + min, + max, + humantime::format_duration(Duration::from_secs(timer.elapsed().as_secs())) + ); + Ok(()) } diff --git a/utilities/traffic-sim/Cargo.toml b/utilities/traffic-sim/Cargo.toml new file mode 100644 index 0000000000..890ed36efd --- /dev/null +++ b/utilities/traffic-sim/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "traffic-sim" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +clap = { version = "4.0", features = ["derive"] } +tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } +anyhow = { workspace = true } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +reqwest = { workspace = true, features = ["json"] } +rand = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } +csv = "1.4.0" + +tari_wallet_daemon_client = { workspace = true } +tari_template_lib = { workspace = true } +tari_ootle_common_types = { workspace = true } +tari_ootle_wallet_sdk = { workspace = true } +tari_transaction = { workspace = true } +tari_indexer_client = { workspace = true, features = ["client"] } diff --git a/utilities/traffic-sim/src/main.rs b/utilities/traffic-sim/src/main.rs new file mode 100644 index 0000000000..d4343b7535 --- /dev/null +++ b/utilities/traffic-sim/src/main.rs @@ -0,0 +1,157 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +mod sim; + +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use log::LevelFilter; +use tari_template_lib::models::{ComponentAddress, ResourceAddress, UtxoId}; + +use crate::sim::TrafficSim; + +#[derive(Parser)] +#[command(name = "traffic-sim")] +#[command(about = "A CLI tool for simulating traffic between wallets in a swarm")] +struct Cli { + #[arg( + long, + default_value = "http://localhost:8080/json_rpc", + help = "URL of the swarm API" + )] + swarm_url: String, + + #[arg( + short = 'x', + long, + default_value = "http://localhost:9000/json_rpc", + help = "URL of the exchange wallet API" + )] + exchange_wallet_url: String, + + #[command(subcommand)] + command: Commands, +} + +impl Cli { + pub fn init() -> Self { + Cli::parse() + } +} + +#[derive(Subcommand)] +enum Commands { + Run { + #[arg(long, default_value_t = 100000000, help = "Minimum transaction value")] + min_value: u64, + + #[arg(long, default_value_t = 200000000, help = "Maximum transaction value")] + max_value: u64, + + #[arg(short = 'r', long, help = "Resource address to use for transactions")] + resource_address: ResourceAddress, + }, + Setup { + #[arg(short = 'b', long, help = "Admin badge resource address")] + admin_badge_resource: ResourceAddress, + + #[arg(short = 'c', long, help = "Faucet component address")] + faucet_component: ComponentAddress, + + #[arg(short = 'r', long, help = "Resource address of the resource to transfer")] + resource_address: ResourceAddress, + }, + ListWallets, + DecryptUtxos { + #[arg(long, default_value_t = 0, help = "Minimum transaction value")] + min_value: u64, + + #[arg(long, default_value_t = 300000000, help = "Maximum transaction value")] + max_value: u64, + #[arg(short = 'r', long, help = "Resource address of the resource to decrypt")] + resource_address: ResourceAddress, + + #[arg(long, help = "Last UTXO ID processed")] + last_id: Option, + + #[arg(long, help = "Specific UTXO ID to decrypt")] + specific_id: Option, + + #[arg(long, alias = "csv", help = "Path to output CSV file with results")] + csv_output: Option, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::builder().filter_level(LevelFilter::Info).init(); + let cli = Cli::init(); + + match cli.command { + Commands::Run { + min_value, + max_value, + resource_address, + } => { + if min_value >= max_value { + return Err(anyhow::anyhow!("min-value must be less than max-value")); + } + + let mut sim = TrafficSim::new(cli.swarm_url, cli.exchange_wallet_url); + sim.run_simulation(resource_address, min_value, max_value).await?; + }, + Commands::Setup { + admin_badge_resource, + faucet_component, + resource_address, + } => { + let mut sim = TrafficSim::new(cli.swarm_url, cli.exchange_wallet_url); + sim.connect_to_wallets().await?; + sim.setup_accounts().await?; + sim.setup_wallet_funds(faucet_component, resource_address, admin_badge_resource) + .await?; + }, + Commands::ListWallets => { + let mut sim = TrafficSim::new(cli.swarm_url, cli.exchange_wallet_url); + sim.connect_to_wallets().await?; + if sim.wallets().is_empty() { + println!("No wallets found in swarm"); + return Ok(()); + } + + println!("Found {} wallets:", sim.wallets().len()); + for (i, wallet) in sim.wallets().iter().enumerate() { + println!("{}) {}", i + 1, wallet.client.endpoint()); + } + }, + Commands::DecryptUtxos { + min_value, + max_value, + resource_address, + last_id, + specific_id, + csv_output, + } => { + let mut sim = TrafficSim::new(cli.swarm_url, cli.exchange_wallet_url); + sim.decrypt_utxos( + min_value, + max_value, + resource_address, + last_id + .as_ref() + .map(|id| UtxoId::from_hex(id).context("Invalid last_id")) + .transpose()?, + specific_id + .as_ref() + .map(|id| UtxoId::from_hex(id).context("Invalid specific_id")) + .transpose()?, + csv_output, + ) + .await?; + }, + } + + Ok(()) +} diff --git a/utilities/traffic-sim/src/sim.rs b/utilities/traffic-sim/src/sim.rs new file mode 100644 index 0000000000..c295b0bfed --- /dev/null +++ b/utilities/traffic-sim/src/sim.rs @@ -0,0 +1,574 @@ +// Copyright 2025 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + time::Duration, +}; + +use rand::Rng; +use reqwest::Client; +use serde_json::json; +use tari_indexer_client::types::ListUtxosRequest; +use tari_ootle_common_types::{displayable::Displayable, optional::Optional, Network}; +use tari_ootle_wallet_sdk::{ + apis::{confidential_transfer::UtxoInputSelection, stealth_transfer::TransferOutput}, + crypto::memo::Memo, + models::{AccountWithAddress, BranchAndKeyId, KeyBranch}, +}; +use tari_template_lib::{ + constants::XTR, + models::{ComponentAddress, ResourceAddress, UtxoId}, + types::Amount, +}; +use tari_transaction::{args, Transaction}; +use tari_wallet_daemon_client::{ + types::{ + AccountsAssociateStealthResourceRequest, + AccountsCreateFreeTestCoinsRequest, + AccountsCreateRequest, + AccountsCreateStealthTransferStatementRequest, + AccountsGetBalancesRequest, + AuthLoginAcceptRequest, + AuthLoginRequest, + InputSelection, + StealthTransfer, + StealthTransferRequest, + StealthUtxosDecryptValueRequest, + TransactionSubmitRequest, + TransactionWaitResultRequest, + TransferStatementRequest, + }, + WalletDaemonClient, +}; +use tokio::time::sleep; + +pub struct Wallet { + pub name: String, + pub client: WalletDaemonClient, + pub network: Network, +} + +impl Wallet { + pub async fn connect(name: String, address: &str) -> anyhow::Result { + let mut client = WalletDaemonClient::connect(address, None)?; + let resp = client + .auth_request(AuthLoginRequest { + permissions: vec!["Admin".to_string()], + duration: Some(Duration::from_secs(3600)), + webauthn_finish_auth_request: None, + }) + .await?; + let resp = client + .auth_accept(AuthLoginAcceptRequest { + auth_token: resp.auth_token, + name: "sim".to_string(), + }) + .await?; + client.set_auth_token(resp.permissions_token); + let settings = client.get_settings().await?; + Ok(Self { + name, + client, + network: settings.network.byte.try_into()?, + }) + } +} + +pub struct TrafficSim { + swarm_url: String, + exchange_wallet_url: String, + wallets: Vec, + accounts: Vec, +} + +impl TrafficSim { + pub fn new(swarm_url: String, exchange_wallet_url: String) -> Self { + Self { + swarm_url, + exchange_wallet_url, + wallets: Vec::new(), + accounts: Vec::new(), + } + } + + pub fn wallet_and_account_iter(&self) -> impl Iterator { + self.wallets.iter().zip(self.accounts.iter()) + } + + pub async fn connect_exchange_wallet(&self) -> anyhow::Result { + Wallet::connect("Exchange".to_string(), &self.exchange_wallet_url).await + } + + pub async fn connect_indexer_client( + &self, + ) -> anyhow::Result { + let mut exchange_wallet = self.connect_exchange_wallet().await?; + let indexer_url = exchange_wallet.client.get_settings().await?.indexer_url; + let client = tari_indexer_client::rest_api_client::IndexerRestApiClient::connect(indexer_url)?; + Ok(client) + } + + pub fn wallets(&self) -> &Vec { + &self.wallets + } + + pub async fn connect_to_wallets(&mut self) -> anyhow::Result<()> { + #[derive(Debug, Clone, serde::Deserialize)] + pub struct InstanceInfo { + #[allow(dead_code)] + pub id: u64, + pub name: String, + pub ports: HashMap, + #[allow(dead_code)] + pub settings: HashMap, + #[allow(dead_code)] + pub base_path: PathBuf, + #[allow(dead_code)] + pub instance_type: String, + pub is_running: bool, + } + + #[derive(serde::Deserialize, Debug)] + struct SwarmResponse { + pub instances: Vec, + } + + let response = Client::new() + .post(&self.swarm_url) + .json(&json!({ + "jsonrpc": "2.0", + "method": "list_instances", + "params": { "by_type": "TariWalletDaemon" }, + "id": 1, + })) + .send() + .await?; + + let swarm_response: serde_json::Value = response.json().await?; + let resp = serde_json::from_value::( + swarm_response + .get("result") + .ok_or_else(|| anyhow::anyhow!("No result field in swarm response"))? + .clone(), + )?; + let addrs = resp.instances.into_iter().filter_map(|instance| { + if !instance.is_running { + log::info!("WARN: stopped wallet instance: {}", instance.name); + } + let rpc_port = instance.ports.get("jrpc")?; + Some((instance.name, format!("http://localhost:{}/json_rpc", rpc_port))) + }); + + for (name, addr) in addrs { + let wallet = Wallet::connect(name, &addr).await?; + self.wallets.push(wallet); + } + + Ok(()) + } + + pub async fn send_random_transaction( + &mut self, + id: usize, + resource_address: ResourceAddress, + min_value: u64, + max_value: u64, + ) -> anyhow::Result<()> { + if self.wallets.len() < 2 { + return Err(anyhow::anyhow!("Need at least 2 wallets to send transactions")); + } + + let mut rng = rand::thread_rng(); + let sender_idx = rng.gen_range(0..self.accounts.len()); + let mut receiver_idx = rng.gen_range(0..self.accounts.len()); + + while receiver_idx == sender_idx { + receiver_idx = rng.gen_range(0..self.accounts.len()); + } + + let sender_account = &self.accounts[sender_idx]; + let sender_wallet = &self.wallets[sender_idx]; + let receiver_address = &self.accounts[receiver_idx]; + let receiver_wallet = &self.wallets[receiver_idx]; + + let amount_to_send = rng.gen_range(min_value..=max_value); + + log::info!( + "Sending {} ootle from {} to {}", + amount_to_send, + sender_wallet.name, + receiver_wallet.name + ); + + let sender = &self.wallets[sender_idx]; + let mut sender_client = sender.client.clone(); + + let resp = sender_client + .accounts_stealth_transfer(StealthTransferRequest { + owner_account: (*sender_account.component_address()).into(), + fee_input_selection: UtxoInputSelection::PreferConfidential, + input_selection: UtxoInputSelection::ConfidentialOnly, + resource_address, + badge_usage: Default::default(), + transfers: vec![StealthTransfer { + destination_address: receiver_address.address().clone(), + blinded_output_amount: amount_to_send.into(), + revealed_output_amount: Default::default(), + output_memo: Some(Memo::new_message(format!("Transfer {id}: {amount_to_send}")).unwrap()), + }], + max_fee: 1000, + dry_run: false, + }) + .await?; + + log::info!( + "Tx {} --> {}: {}", + sender_wallet.name, + receiver_wallet.name, + resp.transaction_id, + ); + + log::info!( + "send transaction of {} from {} to {}", + amount_to_send, + sender_account, + receiver_address + ); + + Ok(()) + } + + #[allow(clippy::too_many_lines)] + pub async fn setup_wallet_funds( + &mut self, + faucet_component: ComponentAddress, + funds_resource_address: ResourceAddress, + admin_resource_address: ResourceAddress, + ) -> anyhow::Result<()> { + let mut exchange_wallet = self.connect_exchange_wallet().await?; + let resp = exchange_wallet.client.accounts_get_default().await?; + let exchange_account = AccountWithAddress::new(resp.account, resp.address); + let exchange_account_key_id = exchange_account + .account + .owner_key_id + .ok_or_else(|| anyhow::anyhow!("Exchange account has no owner key ID"))?; + + for (wallet, account) in self.wallet_and_account_iter() { + let mut client = wallet.client.clone(); + + let fund_amount = 10_000_000_000u64; + + client + .associate_stealth_resource(AccountsAssociateStealthResourceRequest { + account: (*account.component_address()).into(), + resource_address: funds_resource_address, + }) + .await?; + + let balances = client + .get_account_balances(AccountsGetBalancesRequest { + account: Some((*account.component_address()).into()), + refresh: true, + }) + .await + .optional()?; + if balances.as_ref().is_none_or(|b| { + b.balances + .iter() + .all(|b| b.resource_address != XTR || b.balance.is_zero()) + }) { + log::info!("[{}] Funding account with XTR: {}", wallet.name, account); + client + .create_free_test_coins(AccountsCreateFreeTestCoinsRequest { + account: (*account.component_address()).into(), + amount: 1_000_000_000.into(), + max_fee: None, + }) + .await?; + } + if let Some(balance) = balances.as_ref().and_then(|b| { + b.balances + .iter() + .find(|b| b.resource_address == funds_resource_address && b.confidential_balance > 0) + }) { + log::info!( + "[{}] Account already funded: {} (balance: {})", + wallet.name, + account, + balance.confidential_balance + ); + continue; + } + + log::info!("[{}] Converting to stealth UTXOs for account: {}", wallet.name, account); + + let transfer_resp = exchange_wallet + .client + .accounts_create_stealth_transfer_statement(AccountsCreateStealthTransferStatementRequest { + requests: vec![TransferStatementRequest { + // Basically ignored since we are not spending any inputs (but the account must exist) + sender_account: (*exchange_account.component_address()).into(), + resource_address: funds_resource_address, + // Inputs come from a bucket that we provide in the transaction, so none of the wallet's + // vaults/UTXOs are used + input_selection: InputSelection::FromBucket { + revealed_amount: fund_amount.into(), + }, + outputs: vec![TransferOutput { + address: account.address().clone(), + revealed_amount: Amount::zero(), + blinded_amount: fund_amount.into(), + memo: Some(Memo::new_message(format!("Initial Funding: {fund_amount}")).unwrap()), + }], + }], + }) + .await?; + + let transaction = Transaction::builder() + .for_network(wallet.network.as_byte()) + .fee_transaction_pay_from_component(*exchange_account.component_address(), 500) + .call_method(*exchange_account.component_address(), "create_proof_by_amount", args![ + admin_resource_address, + 1 + ]) + .put_last_instruction_output_on_workspace("proof") + .call_method(faucet_component, "withdraw", args![fund_amount]) + .put_last_instruction_output_on_workspace("funds") + .stealth_transfer_with_input_bucket( + funds_resource_address, + transfer_resp.statements[0].clone(), + "funds", + ) + .drop_all_proofs_in_workspace() + .with_authorized_seal_signer() + .build_unsigned_transaction(); + + let resp = exchange_wallet + .client + .submit_transaction(TransactionSubmitRequest { + transaction, + seal_signer: BranchAndKeyId::new(KeyBranch::Account, exchange_account_key_id), + other_signers: transfer_resp.signing_keys, + detect_inputs: true, + detect_inputs_use_unversioned: true, + lock_ids: vec![transfer_resp.lock_id], + }) + .await?; + + let resp = exchange_wallet + .client + .wait_transaction_result(TransactionWaitResultRequest { + transaction_id: resp.transaction_id, + timeout_secs: Some(60), + }) + .await?; + + if !resp.status.is_accepted() { + return Err(anyhow::anyhow!( + "Funding transaction failed for wallet {}: {} {}", + wallet.name, + resp.status, + resp.result.as_ref().and_then(|r| r.result.any_reject()).display() + )); + } + + log::info!( + "[{}] Funded account {} with {} of resource {}", + wallet.name, + account, + fund_amount, + funds_resource_address + ); + } + + Ok(()) + } + + pub async fn run_simulation( + &mut self, + resource_address: ResourceAddress, + min_value: u64, + max_value: u64, + ) -> anyhow::Result<()> { + log::info!("Fetching wallets from swarm at: {}", self.swarm_url); + self.connect_to_wallets().await?; + self.setup_accounts().await?; + + if self.wallets.is_empty() { + return Err(anyhow::anyhow!("No wallets found in swarm")); + } + + log::info!("Found {} wallets", self.wallets.len()); + + log::info!( + "Starting transaction simulation (min: {}, max: {})", + min_value, + max_value + ); + + let mut id = 0; + loop { + id += 1; + match self + .send_random_transaction(id, resource_address, min_value, max_value) + .await + { + Ok(_) => { + let delay = rand::thread_rng().gen_range(1..=5); + sleep(Duration::from_secs(delay)).await; + }, + Err(e) => { + log::info!("Transaction failed: {:?}", e); + sleep(Duration::from_secs(1)).await; + }, + } + } + } + + pub async fn setup_accounts(&mut self) -> anyhow::Result<()> { + for wallet in &mut self.wallets { + let client = &mut wallet.client; + if let Some(acc) = client.accounts_get("sim_default".into()).await.optional()? { + log::info!( + "Found default account for wallet at {}: {}", + client.endpoint(), + acc.account + ); + self.accounts.push(AccountWithAddress { + account: acc.account, + address: acc.address, + }); + } else { + log::info!("No default account found for wallet at {}", client.endpoint()); + let resp = client + .create_account(AccountsCreateRequest { + account_name: Some("sim_default".to_string()), + is_default: Some(true), + key_index: None, + }) + .await?; + log::info!( + "Created default account for wallet at {}: {}", + client.endpoint(), + resp.account + ); + self.accounts.push(AccountWithAddress { + account: resp.account, + address: resp.address, + }); + } + } + Ok(()) + } + + pub async fn decrypt_utxos>( + &mut self, + min_value: u64, + max_value: u64, + resource_address: ResourceAddress, + last_id: Option, + specific_id: Option, + csv_out: Option

, + ) -> anyhow::Result<()> { + let mut indexer = self.connect_indexer_client().await?; + + let resource = indexer + .get_substate(&resource_address.into(), None, true) + .await? + .substate + .into_resource(); + + let divisibility = resource.as_ref().map(|r| r.divisibility()).unwrap_or(6); + let token_symbol = resource + .as_ref() + .and_then(|r| r.token_symbol().map(|s| s.to_string())) + .unwrap_or_default(); + + if let Some(specific_id) = specific_id { + let mut wallet = self.connect_exchange_wallet().await?; + let resp = wallet + .client + .stealth_utxos_decrypt_value(StealthUtxosDecryptValueRequest { + resource_address, + ids: vec![specific_id], + view_key_id: 0, + minimum_expected_value: Some(min_value), + maximum_expected_value: max_value, + }) + .await?; + + log::info!("Decrypted UTXO value:"); + for (id, value) in resp.values { + let value = value + .map(Amount::from) + .map(|a| a.to_decimal_string(divisibility.into())); + log::info!("{}: {} {}", id, value.display(), token_symbol); + } + + return Ok(()); + } + + let resp = indexer + .list_utxos(ListUtxosRequest { + resource_address, + limit: 1000, + from_id: last_id, + }) + .await?; + + log::info!( + "Attempting to decrypt {} UTXOs within range {min_value}-{max_value}", + resp.utxos.len() + ); + if resp.utxos.is_empty() { + return Ok(()); + } + + let utxo_ids = resp + .utxos + .iter() + .filter(|(_, o)| !o.is_burnt()) + .map(|(id, _)| id) + .copied() + .collect::>(); + + let mut wallet = self.connect_exchange_wallet().await?; + let mut csv = csv_out.map(csv::Writer::from_path).transpose()?; + csv.as_mut() + .map(|c| c.write_record(["utxo_id", "decrypted_value"])) + .transpose()?; + + for ids in utxo_ids.chunks(10) { + let resp = wallet + .client + .stealth_utxos_decrypt_value(StealthUtxosDecryptValueRequest { + resource_address, + ids: ids.to_vec(), + view_key_id: 0, + minimum_expected_value: Some(min_value), + maximum_expected_value: max_value, + }) + .await?; + + for (id, value) in resp.values { + log::info!( + "{}: {} {}", + id, + value + .map(Amount::from) + .map(|a| a.to_decimal_string(divisibility.into())) + .display(), + value.map(|_| token_symbol.as_str()).unwrap_or_default() + ); + + csv.as_mut() + .map(|c| c.write_record(&[id.to_string(), value.map(|v| v.to_string()).unwrap_or_default()])) + .transpose()?; + } + } + + Ok(()) + } +}