From 4b2225291eb61955b5ff575f134d68aa47deedfe Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Wed, 9 Feb 2022 07:49:32 +0200 Subject: [PATCH 01/28] fix: improved image handling in collectibles (#3808) Description --- - Adds "back" and "reset" buttons to various parts of image selection in collectibles - Adds a timestamp for the call to `ipfs files cp` in case of duplicate --- .../tari_collectibles/web-app/src/Create.js | 107 ++++++++++++------ 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/applications/tari_collectibles/web-app/src/Create.js b/applications/tari_collectibles/web-app/src/Create.js index 93c5a496d3..31ce44c95f 100644 --- a/applications/tari_collectibles/web-app/src/Create.js +++ b/applications/tari_collectibles/web-app/src/Create.js @@ -56,7 +56,7 @@ class Create extends React.Component { image: "", cid: "", error: "", - ipfsUploadError: null, + imageError: null, isSaving: false, tip001: true, tip002: false, @@ -92,9 +92,9 @@ class Create extends React.Component { try { // only use the first file if multiple are dropped let cid = await this.addFileToIPFS(payload[0]); - this.setState({ cid, ipfsUploadError: null }); + this.setState({ cid, imageError: null }); } catch (e) { - this.setState({ ipfsUploadError: e.toString() }); + this.setState({ imageError: e.toString() }); } } } @@ -294,15 +294,16 @@ class Create extends React.Component { try { let cid = await this.addFileToIPFS(filePath); console.info("IPFS cid: ", cid); - this.setState({ cid, ipfsUploadError: null }); + this.setState({ cid, imageError: null }); } catch (e) { - this.setState({ ipfsUploadError: e.toString() }); + this.setState({ imageError: e.toString() }); } }; addFileToIPFS = async (filePath) => { const parts = filePath.split("/"); const name = parts[parts.length - 1]; + const timestamp = new Date().valueOf(); // unfortunately the ipfs http /add api doesn't play nicely with the tauri http client // resulting in "file argument 'path' is required" // so we'll shell out to the ipfs command @@ -329,7 +330,14 @@ class Create extends React.Component { }); }); - const cp = new Command("ipfs", ["files", "cp", `/ipfs/${cid}`, `/${name}`]); + if (!cid) return; + + const cp = new Command("ipfs", [ + "files", + "cp", + `/ipfs/${cid}`, + `/${timestamp}${name}`, + ]); await cp.spawn(); await new Promise((resolve, reject) => { @@ -347,22 +355,6 @@ class Create extends React.Component { }); return cid; - // console.log("command", command); - // const { success, output, error } = commandOutput(command); - // if (success) { - // const cid = output; - // console.log("cid", cid); - // const command = await runCommand("ipfs", [ - // "files", - // "cp", - // `/ipfs/${cid}`, - // `/${name}`, - // ]); - // const { error } = commandOutput(command); - // if (error) console.error("error: ", error); - // } else { - // console.error("error: ", error); - // } }; render() { @@ -424,7 +416,8 @@ class Create extends React.Component { cid={this.state.cid} setCid={(cid) => this.setState({ cid })} image={this.state.image} - error={this.state.ipfsUploadError} + error={this.state.imageError} + setError={(e) => this.setState({ imageError: e })} /> @@ -550,7 +543,7 @@ ImageSwitch.propTypes = { setMode: PropTypes.func, }; -const ImageUrl = ({ setImage }) => { +const ImageUrl = ({ setImage, setMode }) => { const [url, setUrl] = useState(""); return ( @@ -565,54 +558,78 @@ const ImageUrl = ({ setImage }) => { onChange={(e) => setUrl(e.target.value)} /> + ); }; ImageUrl.propTypes = { setImage: PropTypes.func, + setMode: PropTypes.func, }; -const ImageUpload = ({ selectFile, error }) => { +const ImageUpload = ({ selectFile, setMode }) => { return (

Select an image, or drag and drop an image onto this window

- - {error ? {error} : } +
); }; ImageUpload.propTypes = { selectFile: PropTypes.func, + setMode: PropTypes.func, error: PropTypes.string, }; -const ImageSelector = ({ cid, image, selectFile, setImage, setCid, error }) => { +const ImageSelector = ({ + cid, + image, + selectFile, + setImage, + setCid, + error, + setError, +}) => { const [mode, setMode] = useState(""); + const reset = () => { + setError(""); + setMode(""); + setImage(""); + }; + + if (error) { + return ( +
+ {error} + +
+ ); + } if (image) { return (

-

setImage("")}>Change

+
); } if (cid) { - return ; + return ; } let display; switch (mode) { case "url": - display = ; + display = ; break; case "upload": - display = ; + display = ; break; default: display = ; @@ -628,9 +645,10 @@ ImageSelector.propTypes = { setImage: PropTypes.func, setCid: PropTypes.func, error: PropTypes.string, + setError: PropTypes.func, }; -const IpfsImage = ({ cid, setCid, error }) => { +const IpfsImage = ({ cid, setCid, reset, error }) => { const [src, setSrc] = useState(""); const [httpError, setHttpError] = useState(null); @@ -659,7 +677,15 @@ const IpfsImage = ({ cid, setCid, error }) => {

-

setCid("")}>Change

+
); } @@ -667,7 +693,17 @@ const IpfsImage = ({ cid, setCid, error }) => { if (error || httpError) { return (
- {error || httpError} + + {error || httpError} - is the IPFS daemon running? + +
); } @@ -678,6 +714,7 @@ const IpfsImage = ({ cid, setCid, error }) => { IpfsImage.propTypes = { cid: PropTypes.string, setCid: PropTypes.func, + reset: PropTypes.func, error: PropTypes.string, }; From 2ccf9a8f4584105bcfa47ef3f178fadd7946b74a Mon Sep 17 00:00:00 2001 From: Denis Kolodin Date: Wed, 9 Feb 2022 10:50:32 +0300 Subject: [PATCH 02/28] refactor: split `ErrorCodes` into the `ExitError` and `ExitCode` pair (#3810) Description --- The PR splits `ErrorCodes` into the pair of `ExitCode` enum and `ExitError` struct. Motivation and Context --- The purpose of this change is to separate strings from error codes in order to later use more specific error types instead of strings or `ErrorCodes` directly to have detailed backtraces that will simplify and speed up debugging of the code. How Has This Been Tested? --- CI pass expected --- .../launchpad/backend/src/docker/error.rs | 4 +- .../src/identity_management.rs | 27 +-- .../tari_app_utilities/src/initialization.rs | 4 +- .../tari_app_utilities/src/utilities.rs | 12 +- applications/tari_base_node/src/main.rs | 41 +++-- applications/tari_base_node/src/recovery.rs | 13 +- .../src/automation/error.rs | 10 +- .../tari_console_wallet/src/init/mod.rs | 93 ++++++---- applications/tari_console_wallet/src/main.rs | 26 +-- .../tari_console_wallet/src/recovery.rs | 18 +- .../tari_console_wallet/src/ui/mod.rs | 28 +-- .../tari_console_wallet/src/wallet_modes.rs | 42 +++-- applications/tari_mining_node/src/main.rs | 23 ++- applications/tari_validator_node/src/comms.rs | 17 +- .../tari_validator_node/src/dan_node.rs | 17 +- applications/tari_validator_node/src/main.rs | 29 +-- base_layer/wallet/src/error.rs | 13 +- .../src/output_manager_service/error.rs | 6 +- common/src/exit_codes.rs | 169 +++++++++--------- infrastructure/libtor/src/tor.rs | 14 +- 20 files changed, 340 insertions(+), 266 deletions(-) diff --git a/applications/launchpad/backend/src/docker/error.rs b/applications/launchpad/backend/src/docker/error.rs index 07f4b8a864..7dd20bae56 100644 --- a/applications/launchpad/backend/src/docker/error.rs +++ b/applications/launchpad/backend/src/docker/error.rs @@ -23,7 +23,7 @@ use std::error::Error; -use tari_app_utilities::common::exit_codes::ExitCodes; +use tari_app_utilities::common::exit_codes::ExitError; use thiserror::Error; #[derive(Debug, Error)] @@ -43,7 +43,7 @@ pub enum DockerWrapperError { #[error("It should not be possible to be in this error state")] UnexpectedError, #[error("Could not create an identity file")] - IdentityError(#[from] ExitCodes), + IdentityError(#[from] ExitError), #[error("The specified image type is not supported")] InvalidImageType, } diff --git a/applications/tari_app_utilities/src/identity_management.rs b/applications/tari_app_utilities/src/identity_management.rs index 41fb3d24aa..d16afeca65 100644 --- a/applications/tari_app_utilities/src/identity_management.rs +++ b/applications/tari_app_utilities/src/identity_management.rs @@ -27,7 +27,7 @@ use rand::rngs::OsRng; use serde::{de::DeserializeOwned, Serialize}; use tari_common::{ configuration::{bootstrap::prompt, utils::get_local_ip}, - exit_codes::ExitCodes, + exit_codes::{ExitCode, ExitError}, }; use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, NodeIdentity}; use tari_crypto::tari_utilities::hex::Hex; @@ -48,7 +48,7 @@ pub fn setup_node_identity>( public_address: &Option, create_id: bool, peer_features: PeerFeatures, -) -> Result, ExitCodes> { +) -> Result, ExitError> { match load_identity(&identity_file) { Ok(id) => match public_address { Some(public_address) => { @@ -69,12 +69,15 @@ pub fn setup_node_identity>( identity.", e ); - return Err(ExitCodes::ConfigError(format!( - "Node identity information not found. {}. You can update the configuration file to point to a \ - valid node identity file, or re-run the node with the --create-id flag to create a new \ - identity.", - e - ))); + return Err(ExitError::new( + ExitCode::ConfigError, + format!( + "Node identity information not found. {}. You can update the configuration file to point \ + to a valid node identity file, or re-run the node with the --create-id flag to create a \ + new identity.", + e + ), + )); }; } @@ -93,10 +96,10 @@ pub fn setup_node_identity>( }, Err(e) => { error!(target: LOG_TARGET, "Could not create new node id. {:?}.", e); - Err(ExitCodes::ConfigError(format!( - "Could not create new node id. {:?}.", - e - ))) + Err(ExitError::new( + ExitCode::ConfigError, + format!("Could not create new node id. {:?}.", e), + )) }, } }, diff --git a/applications/tari_app_utilities/src/initialization.rs b/applications/tari_app_utilities/src/initialization.rs index c82c0520ab..0f37b67c87 100644 --- a/applications/tari_app_utilities/src/initialization.rs +++ b/applications/tari_app_utilities/src/initialization.rs @@ -4,7 +4,7 @@ use config::Config; use structopt::StructOpt; use tari_common::{ configuration::{bootstrap::ApplicationType, Network}, - exit_codes::ExitCodes, + exit_codes::ExitError, ConfigBootstrap, DatabaseType, GlobalConfig, @@ -16,7 +16,7 @@ pub const LOG_TARGET: &str = "tari::application"; pub fn init_configuration( application_type: ApplicationType, -) -> Result<(ConfigBootstrap, GlobalConfig, Config), ExitCodes> { +) -> Result<(ConfigBootstrap, GlobalConfig, Config), ExitError> { // Parse and validate command-line arguments let mut bootstrap = ConfigBootstrap::from_args(); diff --git a/applications/tari_app_utilities/src/utilities.rs b/applications/tari_app_utilities/src/utilities.rs index 2c38e65550..7d60904721 100644 --- a/applications/tari_app_utilities/src/utilities.rs +++ b/applications/tari_app_utilities/src/utilities.rs @@ -24,7 +24,13 @@ use std::sync::Arc; use futures::future::Either; use log::*; -use tari_common::{exit_codes::ExitCodes, CommsTransport, GlobalConfig, SocksAuthentication, TorControlAuthentication}; +use tari_common::{ + exit_codes::{ExitCode, ExitError}, + CommsTransport, + GlobalConfig, + SocksAuthentication, + TorControlAuthentication, +}; use tari_common_types::{emoji::EmojiId, types::BlockHash}; use tari_comms::{ peer_manager::NodeId, @@ -145,7 +151,7 @@ pub fn convert_socks_authentication(auth: SocksAuthentication) -> socks::Authent /// /// ## Returns /// A result containing the runtime on success, string indicating the error on failure -pub fn setup_runtime(config: &GlobalConfig) -> Result { +pub fn setup_runtime(config: &GlobalConfig) -> Result { let mut builder = runtime::Builder::new_multi_thread(); if let Some(core_threads) = config.core_threads { @@ -163,7 +169,7 @@ pub fn setup_runtime(config: &GlobalConfig) -> Result { builder.enable_all().build().map_err(|e| { let msg = format!("There was an error while building the node runtime. {}", e); - ExitCodes::UnknownError(msg) + ExitError::new(ExitCode::UnknownError, msg) }) } diff --git a/applications/tari_base_node/src/main.rs b/applications/tari_base_node/src/main.rs index 1d1b2c360c..7ed6bee243 100644 --- a/applications/tari_base_node/src/main.rs +++ b/applications/tari_base_node/src/main.rs @@ -120,7 +120,12 @@ use tari_app_utilities::{ }; #[cfg(all(unix, feature = "libtor"))] use tari_common::CommsTransport; -use tari_common::{configuration::bootstrap::ApplicationType, exit_codes::ExitCodes, ConfigBootstrap, GlobalConfig}; +use tari_common::{ + configuration::bootstrap::ApplicationType, + exit_codes::{ExitCode, ExitError}, + ConfigBootstrap, + GlobalConfig, +}; use tari_comms::{ peer_manager::PeerFeatures, tor::HiddenServiceControllerError, @@ -146,19 +151,19 @@ const LOG_TARGET: &str = "base_node::app"; /// Application entry point fn main() { - if let Err(exit_code) = main_inner() { - exit_code.eprint_details(); + if let Err(err) = main_inner() { + eprintln!("{:?}", err); + let exit_code = err.exit_code; + eprintln!("{}", exit_code.hint()); error!( target: LOG_TARGET, - "Exiting with code ({}): {:?}", - exit_code.as_i32(), - exit_code + "Exiting with code ({}): {:?}", exit_code as i32, err ); - process::exit(exit_code.as_i32()); + process::exit(exit_code as i32); } } -fn main_inner() -> Result<(), ExitCodes> { +fn main_inner() -> Result<(), ExitError> { #[allow(unused_mut)] // config isn't mutated on windows let (bootstrap, mut config, _) = init_configuration(ApplicationType::BaseNode)?; debug!(target: LOG_TARGET, "Using configuration: {:?}", config); @@ -220,7 +225,7 @@ async fn run_node( config: Arc, bootstrap: ConfigBootstrap, shutdown: Shutdown, -) -> Result<(), ExitCodes> { +) -> Result<(), ExitError> { if bootstrap.tracing_enabled { enable_tracing(); } @@ -244,7 +249,7 @@ async fn run_node( recovery::initiate_recover_db(&config)?; recovery::run_recovery(&config) .await - .map_err(|e| ExitCodes::RecoveryError(e.to_string()))?; + .map_err(|e| ExitError::new(ExitCode::RecoveryError, e))?; return Ok(()); }; @@ -259,29 +264,29 @@ async fn run_node( .map_err(|err| { for boxed_error in err.chain() { if let Some(HiddenServiceControllerError::TorControlPortOffline) = boxed_error.downcast_ref() { - return ExitCodes::TorOffline; + return ExitCode::TorOffline.into(); } if let Some(ChainStorageError::DatabaseResyncRequired(reason)) = boxed_error.downcast_ref() { - return ExitCodes::DbInconsistentState(format!( - "You may need to resync your database because {}", - reason - )); + return ExitError::new( + ExitCode::DbInconsistentState, + format!("You may need to resync your database because {}", reason), + ); } // todo: find a better way to do this if boxed_error.to_string().contains("Invalid force sync peer") { println!("Please check your force sync peers configuration"); - return ExitCodes::ConfigError(boxed_error.to_string()); + return ExitError::new(ExitCode::ConfigError, boxed_error); } } - ExitCodes::UnknownError(err.to_string()) + ExitError::new(ExitCode::UnknownError, err) })?; if let Some(ref base_node_config) = config.base_node_config { if let Some(ref address) = base_node_config.grpc_address { // Go, GRPC, go go let grpc = crate::grpc::base_node_grpc_server::BaseNodeGrpcServer::from_base_node_context(&ctx); - let socket_addr = multiaddr_to_socketaddr(address).map_err(|e| ExitCodes::ConfigError(e.to_string()))?; + let socket_addr = multiaddr_to_socketaddr(address).map_err(|e| ExitError::new(ExitCode::ConfigError, e))?; task::spawn(run_grpc(grpc, socket_addr, shutdown.to_signal())); } } diff --git a/applications/tari_base_node/src/recovery.rs b/applications/tari_base_node/src/recovery.rs index 280d376a03..648dd94b10 100644 --- a/applications/tari_base_node/src/recovery.rs +++ b/applications/tari_base_node/src/recovery.rs @@ -30,7 +30,12 @@ use std::{ use anyhow::anyhow; use log::*; -use tari_common::{configuration::Network, exit_codes::ExitCodes, DatabaseType, GlobalConfig}; +use tari_common::{ + configuration::Network, + exit_codes::{ExitCode, ExitError}, + DatabaseType, + GlobalConfig, +}; use tari_core::{ chain_storage::{ async_db::AsyncBlockchainDb, @@ -54,19 +59,19 @@ use tari_core::{ pub const LOG_TARGET: &str = "base_node::app"; -pub fn initiate_recover_db(node_config: &GlobalConfig) -> Result<(), ExitCodes> { +pub fn initiate_recover_db(node_config: &GlobalConfig) -> Result<(), ExitError> { // create recovery db match &node_config.db_type { DatabaseType::LMDB(p) => { let _backend = create_recovery_lmdb_database(&p).map_err(|err| { error!(target: LOG_TARGET, "{}", err); - ExitCodes::UnknownError(err.to_string()) + ExitError::new(ExitCode::UnknownError, err) })?; }, _ => { const MSG: &str = "Recovery mode is only available for LMDB"; error!(target: LOG_TARGET, "{}", MSG); - return Err(ExitCodes::UnknownError(MSG.to_string())); + return Err(ExitError::new(ExitCode::UnknownError, MSG)); }, }; Ok(()) diff --git a/applications/tari_console_wallet/src/automation/error.rs b/applications/tari_console_wallet/src/automation/error.rs index 617887a6df..bc0d114361 100644 --- a/applications/tari_console_wallet/src/automation/error.rs +++ b/applications/tari_console_wallet/src/automation/error.rs @@ -23,7 +23,7 @@ use std::num::{ParseFloatError, ParseIntError}; use log::*; -use tari_common::exit_codes::ExitCodes; +use tari_common::exit_codes::{ExitCode, ExitError}; use tari_core::transactions::{ tari_amount::{MicroTariError, TariConversionError}, transaction::TransactionError, @@ -70,10 +70,10 @@ pub enum CommandError { ShaError(String), } -impl From for ExitCodes { +impl From for ExitError { fn from(err: CommandError) -> Self { error!(target: LOG_TARGET, "{}", err); - Self::CommandError(err.to_string()) + Self::new(ExitCode::CommandError, err) } } @@ -103,10 +103,10 @@ pub enum ParseError { Unimplemented(String), } -impl From for ExitCodes { +impl From for ExitError { fn from(err: ParseError) -> Self { error!(target: LOG_TARGET, "{}", err); let msg = format!("Failed to parse input file commands! {}", err); - Self::InputError(msg) + Self::new(ExitCode::InputError, msg) } } diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 96644b0f04..2bfff7227c 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -26,7 +26,11 @@ use log::*; use rpassword::prompt_password_stdout; use rustyline::Editor; use tari_app_utilities::utilities::create_transport_type; -use tari_common::{exit_codes::ExitCodes, ConfigBootstrap, GlobalConfig}; +use tari_common::{ + exit_codes::{ExitCode, ExitError}, + ConfigBootstrap, + GlobalConfig, +}; use tari_comms::{ multiaddr::Multiaddr, peer_manager::{Peer, PeerFeatures}, @@ -79,7 +83,7 @@ pub enum WalletBoot { pub fn get_or_prompt_password( arg_password: Option, config_password: Option, -) -> Result, ExitCodes> { +) -> Result, ExitError> { if arg_password.is_some() { return Ok(arg_password); } @@ -88,7 +92,7 @@ pub fn get_or_prompt_password( if let Some(p) = env { let env_password = Some( p.into_string() - .map_err(|_| ExitCodes::IOError("Failed to convert OsString into String".to_string()))?, + .map_err(|_| ExitError::new(ExitCode::IOError, "Failed to convert OsString into String"))?, ); return Ok(env_password); } @@ -102,9 +106,9 @@ pub fn get_or_prompt_password( Ok(Some(password)) } -fn prompt_password(prompt: &str) -> Result { +fn prompt_password(prompt: &str) -> Result { let password = loop { - let pass = prompt_password_stdout(prompt).map_err(|e| ExitCodes::IOError(e.to_string()))?; + let pass = prompt_password_stdout(prompt).map_err(|e| ExitError::new(ExitCode::IOError, e))?; if pass.is_empty() { println!("Password cannot be empty!"); continue; @@ -121,25 +125,25 @@ pub async fn change_password( config: &GlobalConfig, arg_password: Option, shutdown_signal: ShutdownSignal, -) -> Result<(), ExitCodes> { +) -> Result<(), ExitError> { let mut wallet = init_wallet(config, arg_password, None, None, shutdown_signal).await?; let passphrase = prompt_password("New wallet password: ")?; let confirmed = prompt_password("Confirm new password: ")?; if passphrase != confirmed { - return Err(ExitCodes::InputError("Passwords don't match!".to_string())); + return Err(ExitError::new(ExitCode::InputError, "Passwords don't match!")); } wallet .remove_encryption() .await - .map_err(|e| ExitCodes::WalletError(e.to_string()))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, e))?; wallet .apply_encryption(passphrase) .await - .map_err(|e| ExitCodes::WalletError(e.to_string()))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, e))?; println!("Wallet password changed successfully."); @@ -153,7 +157,7 @@ pub async fn change_password( pub async fn get_base_node_peer_config( config: &GlobalConfig, wallet: &mut WalletSqlite, -) -> Result { +) -> Result { // custom let mut base_node_custom = get_custom_base_node_peer_from_db(wallet).await; @@ -163,7 +167,10 @@ pub async fn get_base_node_peer_config( base_node_custom = Some(Peer::from(node)); }, Err(err) => { - return Err(ExitCodes::ConfigError(format!("Malformed custom base node: {}", err))); + return Err(ExitError::new( + ExitCode::ConfigError, + format!("Malformed custom base node: {}", err), + )); }, } } @@ -175,7 +182,7 @@ pub async fn get_base_node_peer_config( .map(|s| SeedPeer::from_str(s)) .map(|r| r.map(Peer::from)) .collect::, _>>() - .map_err(|err| ExitCodes::ConfigError(format!("Malformed base node peer: {}", err)))?; + .map_err(|err| ExitError::new(ExitCode::ConfigError, format!("Malformed base node peer: {}", err)))?; // peer seeds let peer_seeds = config @@ -184,7 +191,7 @@ pub async fn get_base_node_peer_config( .map(|s| SeedPeer::from_str(s)) .map(|r| r.map(Peer::from)) .collect::, _>>() - .map_err(|err| ExitCodes::ConfigError(format!("Malformed seed peer: {}", err)))?; + .map_err(|err| ExitError::new(ExitCode::ConfigError, format!("Malformed seed peer: {}", err)))?; let peer_config = PeerConfig::new(base_node_custom, base_node_peers, peer_seeds); debug!(target: LOG_TARGET, "base node peer config: {:?}", peer_config); @@ -222,7 +229,7 @@ pub fn wallet_mode(bootstrap: &ConfigBootstrap, boot_mode: WalletBoot) -> Wallet } /// Get the notify program script path from config bootstrap or global config if provided -pub fn get_notify_script(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> Result, ExitCodes> { +pub fn get_notify_script(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> Result, ExitError> { debug!(target: LOG_TARGET, "Checking args and config for notify script."); let notify_script = match (&bootstrap.wallet_notify, &config.console_wallet_notify_file) { @@ -255,7 +262,7 @@ pub fn get_notify_script(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> if let Some(path) = ¬ify_script { if !path.exists() { let error = format!("Wallet notify script does not exist at path: {:#?}", path); - return Err(ExitCodes::ConfigError(error)); + return Err(ExitError::new(ExitCode::ConfigError, error)); } } @@ -269,16 +276,16 @@ pub async fn init_wallet( seed_words_file_name: Option, recovery_seed: Option, shutdown_signal: ShutdownSignal, -) -> Result { +) -> Result { fs::create_dir_all( &config .console_wallet_db_file .parent() .expect("console_wallet_db_file cannot be set to a root directory"), ) - .map_err(|e| ExitCodes::WalletError(format!("Error creating Wallet folder. {}", e)))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, format!("Error creating Wallet folder. {}", e)))?; fs::create_dir_all(&config.console_wallet_peer_db_path) - .map_err(|e| ExitCodes::WalletError(format!("Error creating peer db folder. {}", e)))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, format!("Error creating peer db folder. {}", e)))?; debug!(target: LOG_TARGET, "Running Wallet database migrations"); @@ -460,9 +467,9 @@ pub async fn init_wallet( .await .map_err(|e| { if let WalletError::CommsInitializationError(e) = e { - ExitCodes::WalletError(e.to_friendly_string()) + ExitError::new(ExitCode::WalletError, e.to_friendly_string()) } else { - ExitCodes::WalletError(format!("Error creating Wallet Container: {}", e)) + ExitError::new(ExitCode::WalletError, format!("Error creating Wallet Container: {}", e)) } })?; if let Some(hs) = wallet.comms.hidden_service() { @@ -470,7 +477,7 @@ pub async fn init_wallet( .db .set_tor_identity(hs.tor_identity().clone()) .await - .map_err(|e| ExitCodes::WalletError(format!("Problem writing tor identity. {}", e)))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, format!("Problem writing tor identity. {}", e)))?; } if !wallet_encrypted { @@ -487,7 +494,7 @@ pub async fn init_wallet( let confirmed = prompt_password("Confirm wallet password: ")?; if password != confirmed { - return Err(ExitCodes::InputError("Passwords don't match!".to_string())); + return Err(ExitError::new(ExitCode::InputError, "Passwords don't match!")); } (password, true) @@ -510,8 +517,12 @@ pub async fn init_wallet( } if let Some(file_name) = seed_words_file_name { let seed_words = wallet.output_manager_service.get_seed_words().await?.join(" "); - let _ = fs::write(file_name, seed_words) - .map_err(|e| ExitCodes::WalletError(format!("Problem writing seed words to file: {}", e))); + let _ = fs::write(file_name, seed_words).map_err(|e| { + ExitError::new( + ExitCode::WalletError, + format!("Problem writing seed words to file: {}", e), + ) + }); }; Ok(wallet) @@ -522,19 +533,24 @@ pub async fn start_wallet( wallet: &mut WalletSqlite, base_node: &Peer, wallet_mode: &WalletMode, -) -> Result<(), ExitCodes> { +) -> Result<(), ExitError> { // TODO gRPC interfaces for setting base node debug!(target: LOG_TARGET, "Setting base node peer"); let net_address = base_node .addresses .first() - .ok_or_else(|| ExitCodes::ConfigError("Configured base node has no address!".to_string()))?; + .ok_or_else(|| ExitError::new(ExitCode::ConfigError, "Configured base node has no address!"))?; wallet .set_base_node_peer(base_node.public_key.clone(), net_address.address.clone()) .await - .map_err(|e| ExitCodes::WalletError(format!("Error setting wallet base node peer. {}", e)))?; + .map_err(|e| { + ExitError::new( + ExitCode::WalletError, + format!("Error setting wallet base node peer. {}", e), + ) + })?; // Restart transaction protocols if not running in script or command modes @@ -555,12 +571,12 @@ pub async fn start_wallet( Ok(()) } -async fn validate_txos(wallet: &mut WalletSqlite) -> Result<(), ExitCodes> { +async fn validate_txos(wallet: &mut WalletSqlite) -> Result<(), ExitError> { debug!(target: LOG_TARGET, "Starting TXO validations."); wallet.output_manager_service.validate_txos().await.map_err(|e| { error!(target: LOG_TARGET, "Error validating Unspent TXOs: {}", e); - ExitCodes::WalletError(e.to_string()) + ExitError::new(ExitCode::WalletError, e) })?; debug!(target: LOG_TARGET, "TXO validations started."); @@ -568,7 +584,7 @@ async fn validate_txos(wallet: &mut WalletSqlite) -> Result<(), ExitCodes> { Ok(()) } -async fn confirm_seed_words(wallet: &mut WalletSqlite) -> Result<(), ExitCodes> { +async fn confirm_seed_words(wallet: &mut WalletSqlite) -> Result<(), ExitError> { let seed_words = wallet.output_manager_service.get_seed_words().await?; println!(); @@ -595,7 +611,7 @@ async fn confirm_seed_words(wallet: &mut WalletSqlite) -> Result<(), ExitCodes> _ => continue, }, Err(e) => { - return Err(ExitCodes::IOError(e.to_string())); + return Err(ExitError::new(ExitCode::IOError, e)); }, } } @@ -621,16 +637,19 @@ pub fn tari_splash_screen(heading: &str) { /// Prompts the user for a new wallet or to recover an existing wallet. /// Returns the wallet bootmode indicating if it's a new or existing wallet, or if recovery is required. -pub(crate) fn boot(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> Result { +pub(crate) fn boot(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> Result { let wallet_exists = config.console_wallet_db_file.exists(); // forced recovery if bootstrap.recovery { if wallet_exists { - return Err(ExitCodes::RecoveryError(format!( - "Wallet already exists at {:#?}. Remove it if you really want to run recovery in this directory!", - config.console_wallet_db_file - ))); + return Err(ExitError::new( + ExitCode::RecoveryError, + format!( + "Wallet already exists at {:#?}. Remove it if you really want to run recovery in this directory!", + config.console_wallet_db_file + ), + )); } return Ok(WalletBoot::Recovery); } @@ -666,7 +685,7 @@ pub(crate) fn boot(bootstrap: &ConfigBootstrap, config: &GlobalConfig) -> Result } }, Err(e) => { - return Err(ExitCodes::IOError(e.to_string())); + return Err(ExitError::new(ExitCode::IOError, e)); }, } } diff --git a/applications/tari_console_wallet/src/main.rs b/applications/tari_console_wallet/src/main.rs index 1ac1cd42a8..ccc6c6bbe6 100644 --- a/applications/tari_console_wallet/src/main.rs +++ b/applications/tari_console_wallet/src/main.rs @@ -48,7 +48,11 @@ use recovery::prompt_private_key_from_seed_words; use tari_app_utilities::{consts, initialization::init_configuration}; #[cfg(all(unix, feature = "libtor"))] use tari_common::CommsTransport; -use tari_common::{configuration::bootstrap::ApplicationType, exit_codes::ExitCodes, ConfigBootstrap}; +use tari_common::{ + configuration::bootstrap::ApplicationType, + exit_codes::{ExitCode, ExitError}, + ConfigBootstrap, +}; use tari_key_manager::cipher_seed::CipherSeed; #[cfg(all(unix, feature = "libtor"))] use tari_libtor::tor::Tor; @@ -73,20 +77,19 @@ pub mod wallet_modes; fn main() { match main_inner() { Ok(_) => process::exit(0), - Err(exit_code) => { - eprintln!("{:?}", exit_code); + Err(err) => { + eprintln!("{:?}", err); + let exit_code = err.exit_code; error!( target: LOG_TARGET, - "Exiting with code ({:?}): {:?}", - exit_code.as_i32(), - exit_code + "Exiting with code ({}): {:?}", exit_code as i32, exit_code ); - process::exit(exit_code.as_i32()) + process::exit(exit_code as i32) }, } } -fn main_inner() -> Result<(), ExitCodes> { +fn main_inner() -> Result<(), ExitError> { let runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() @@ -197,8 +200,9 @@ fn main_inner() -> Result<(), ExitCodes> { WalletMode::Script(path) => script_mode(config, wallet.clone(), path), WalletMode::Command(command) => command_mode(config, wallet.clone(), command), WalletMode::RecoveryDaemon | WalletMode::RecoveryTui => recovery_mode(config, wallet.clone()), - WalletMode::Invalid => Err(ExitCodes::InputError( - "Invalid wallet mode - are you trying too many command options at once?".to_string(), + WalletMode::Invalid => Err(ExitError::new( + ExitCode::InputError, + "Invalid wallet mode - are you trying too many command options at once?", )), }; @@ -210,7 +214,7 @@ fn main_inner() -> Result<(), ExitCodes> { result } -fn get_recovery_seed(boot_mode: WalletBoot, bootstrap: &ConfigBootstrap) -> Result, ExitCodes> { +fn get_recovery_seed(boot_mode: WalletBoot, bootstrap: &ConfigBootstrap) -> Result, ExitError> { if matches!(boot_mode, WalletBoot::Recovery) { let seed = if bootstrap.seed_words.is_some() { let seed_words: Vec = bootstrap diff --git a/applications/tari_console_wallet/src/recovery.rs b/applications/tari_console_wallet/src/recovery.rs index 54b01a8571..0b81b25b73 100644 --- a/applications/tari_console_wallet/src/recovery.rs +++ b/applications/tari_console_wallet/src/recovery.rs @@ -24,7 +24,7 @@ use chrono::offset::Local; use futures::FutureExt; use log::*; use rustyline::Editor; -use tari_common::exit_codes::ExitCodes; +use tari_common::exit_codes::{ExitCode, ExitError}; use tari_crypto::tari_utilities::hex::Hex; use tari_key_manager::{cipher_seed::CipherSeed, mnemonic::Mnemonic}; use tari_shutdown::Shutdown; @@ -40,7 +40,7 @@ use crate::wallet_modes::PeerConfig; pub const LOG_TARGET: &str = "wallet::recovery"; /// Prompt the user to input their seed words in a single line. -pub fn prompt_private_key_from_seed_words() -> Result { +pub fn prompt_private_key_from_seed_words() -> Result { debug!(target: LOG_TARGET, "Prompting for seed words."); let mut rl = Editor::<()>::new(); @@ -48,7 +48,7 @@ pub fn prompt_private_key_from_seed_words() -> Result { println!("Recovery Mode"); println!(); println!("Type or paste all of your seed words on one line, only separated by spaces."); - let input = rl.readline(">> ").map_err(|e| ExitCodes::IOError(e.to_string()))?; + let input = rl.readline(">> ").map_err(|e| ExitError::new(ExitCode::IOError, e))?; let seed_words: Vec = input.split_whitespace().map(str::to_string).collect(); match CipherSeed::from_mnemonic(&seed_words, None) { @@ -63,14 +63,14 @@ pub fn prompt_private_key_from_seed_words() -> Result { } /// Return seed matching the seed words. -pub fn get_seed_from_seed_words(seed_words: Vec) -> Result { +pub fn get_seed_from_seed_words(seed_words: Vec) -> Result { debug!(target: LOG_TARGET, "Return seed derived from the provided seed words"); match CipherSeed::from_mnemonic(&seed_words, None) { Ok(seed) => Ok(seed), Err(e) => { let err_msg = format!("MnemonicError parsing seed words: {}", e); warn!(target: LOG_TARGET, "{}", err_msg); - Err(ExitCodes::RecoveryError(err_msg)) + Err(ExitError::new(ExitCode::RecoveryError, err_msg)) }, } } @@ -82,7 +82,7 @@ pub async fn wallet_recovery( wallet: &WalletSqlite, base_node_config: &PeerConfig, retry_limit: usize, -) -> Result<(), ExitCodes> { +) -> Result<(), ExitError> { println!("\nPress Ctrl-C to stop the recovery process\n"); // We dont care about the shutdown signal here, so we just create one let shutdown = Shutdown::new(); @@ -103,7 +103,7 @@ pub async fn wallet_recovery( peer_manager .add_peer(peer) .await - .map_err(|err| ExitCodes::NetworkError(err.to_string()))?; + .map_err(|err| ExitError::new(ExitCode::NetworkError, err))?; } let mut recovery_task = UtxoScannerService::::builder() @@ -200,6 +200,6 @@ pub async fn wallet_recovery( recovery_join_handle .await - .map_err(|e| ExitCodes::RecoveryError(format!("{}", e)))? - .map_err(|e| ExitCodes::RecoveryError(format!("{}", e))) + .map_err(|e| ExitError::new(ExitCode::RecoveryError, e))? + .map_err(|e| ExitError::new(ExitCode::RecoveryError, e)) } diff --git a/applications/tari_console_wallet/src/ui/mod.rs b/applications/tari_console_wallet/src/ui/mod.rs index bdb00f239d..24f0f6ed33 100644 --- a/applications/tari_console_wallet/src/ui/mod.rs +++ b/applications/tari_console_wallet/src/ui/mod.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use log::error; -use tari_common::exit_codes::ExitCodes; +use tari_common::exit_codes::{ExitCode, ExitError}; use crate::utils::crossterm_events::CrosstermEvents; @@ -52,7 +52,7 @@ use crate::utils::events::{Event, EventStream}; pub const MAX_WIDTH: u16 = 133; -pub fn run(app: App>) -> Result<(), ExitCodes> { +pub fn run(app: App>) -> Result<(), ExitError> { let mut app = app; Handle::current() .block_on(async { @@ -74,42 +74,42 @@ pub fn run(app: App>) -> Result<(), ExitCodes> { app.app_state.start_event_monitor(app.notifier.clone()).await; Result::<_, UiError>::Ok(()) }) - .map_err(|e| ExitCodes::WalletError(e.to_string()))?; + .map_err(|e| ExitError::new(ExitCode::WalletError, e))?; crossterm_loop(app) } /// This is the main loop of the application UI using Crossterm based events -fn crossterm_loop(mut app: App>) -> Result<(), ExitCodes> { +fn crossterm_loop(mut app: App>) -> Result<(), ExitError> { let events = CrosstermEvents::new(); enable_raw_mode().map_err(|e| { error!(target: LOG_TARGET, "Error enabling Raw Mode {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; let mut stdout = stdout(); execute!(stdout, EnterAlternateScreen).map_err(|e| { error!(target: LOG_TARGET, "Error creating stdout context. {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend).map_err(|e| { error!(target: LOG_TARGET, "Error creating Terminal context. {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; terminal.clear().map_err(|e| { error!(target: LOG_TARGET, "Error clearing interface. {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; loop { terminal.draw(|f| app.draw(f)).map_err(|e| { error!(target: LOG_TARGET, "Error drawing interface. {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; match events.next().map_err(|e| { error!(target: LOG_TARGET, "Error reading input event: {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })? { Event::Input(event) => match (event.code, event.modifiers) { (KeyCode::Char(c), KeyModifiers::CONTROL) => app.on_control_key(c), @@ -137,20 +137,20 @@ fn crossterm_loop(mut app: App>) -> Result<(), ExitCode terminal.clear().map_err(|e| { error!(target: LOG_TARGET, "Error clearing interface. {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; disable_raw_mode().map_err(|e| { error!(target: LOG_TARGET, "Error disabling Raw Mode {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; execute!(terminal.backend_mut(), LeaveAlternateScreen).map_err(|e| { error!(target: LOG_TARGET, "Error releasing stdout {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; terminal.show_cursor().map_err(|e| { error!(target: LOG_TARGET, "Error showing cursor: {}", e); - ExitCodes::InterfaceError + ExitCode::InterfaceError })?; Ok(()) diff --git a/applications/tari_console_wallet/src/wallet_modes.rs b/applications/tari_console_wallet/src/wallet_modes.rs index 60b40f7247..8cd92c1b3b 100644 --- a/applications/tari_console_wallet/src/wallet_modes.rs +++ b/applications/tari_console_wallet/src/wallet_modes.rs @@ -23,7 +23,11 @@ use std::{fs, io::Stdout, path::PathBuf}; use log::*; use rand::{rngs::OsRng, seq::SliceRandom}; -use tari_common::{exit_codes::ExitCodes, ConfigBootstrap, GlobalConfig}; +use tari_common::{ + exit_codes::{ExitCode, ExitError}, + ConfigBootstrap, + GlobalConfig, +}; use tari_comms::{multiaddr::Multiaddr, peer_manager::Peer, utils::multiaddr::multiaddr_to_socketaddr}; use tari_wallet::WalletSqlite; use tokio::runtime::Handle; @@ -85,25 +89,26 @@ impl PeerConfig { /// 1. Custom Base Node /// 2. First configured Base Node Peer /// 3. Random configured Peer Seed - pub fn get_base_node_peer(&self) -> Result { + pub fn get_base_node_peer(&self) -> Result { if let Some(base_node) = self.base_node_custom.clone() { Ok(base_node) } else if !self.base_node_peers.is_empty() { Ok(self .base_node_peers .first() - .ok_or_else(|| ExitCodes::ConfigError("Configured base node peer has no address!".to_string()))? + .ok_or_else(|| ExitError::new(ExitCode::ConfigError, "Configured base node peer has no address!"))? .clone()) } else if !self.peer_seeds.is_empty() { // pick a random peer seed Ok(self .peer_seeds .choose(&mut OsRng) - .ok_or_else(|| ExitCodes::ConfigError("Peer seeds was empty.".to_string()))? + .ok_or_else(|| ExitError::new(ExitCode::ConfigError, "Peer seeds was empty."))? .clone()) } else { - Err(ExitCodes::ConfigError( - "No peer seeds or base node peer defined in config!".to_string(), + Err(ExitError::new( + ExitCode::ConfigError, + "No peer seeds or base node peer defined in config!", )) } } @@ -129,7 +134,7 @@ impl PeerConfig { } } -pub fn command_mode(config: WalletModeConfig, wallet: WalletSqlite, command: String) -> Result<(), ExitCodes> { +pub fn command_mode(config: WalletModeConfig, wallet: WalletSqlite, command: String) -> Result<(), ExitError> { let WalletModeConfig { global_config, handle, .. } = config.clone(); @@ -151,16 +156,16 @@ pub fn command_mode(config: WalletModeConfig, wallet: WalletSqlite, command: Str wallet_or_exit(config, wallet) } -pub fn script_mode(config: WalletModeConfig, wallet: WalletSqlite, path: PathBuf) -> Result<(), ExitCodes> { +pub fn script_mode(config: WalletModeConfig, wallet: WalletSqlite, path: PathBuf) -> Result<(), ExitError> { let WalletModeConfig { global_config, handle, .. } = config.clone(); info!(target: LOG_TARGET, "Starting wallet script mode"); println!("Starting wallet script mode"); - let script = fs::read_to_string(path).map_err(|e| ExitCodes::InputError(e.to_string()))?; + let script = fs::read_to_string(path).map_err(|e| ExitError::new(ExitCode::InputError, e))?; if script.is_empty() { - return Err(ExitCodes::InputError("Input file is empty!".to_string())); + return Err(ExitError::new(ExitCode::InputError, "Input file is empty!")); }; let mut commands = Vec::new(); @@ -192,7 +197,7 @@ pub fn script_mode(config: WalletModeConfig, wallet: WalletSqlite, path: PathBuf } /// Prompts the user to continue to the wallet, or exit. -fn wallet_or_exit(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitCodes> { +fn wallet_or_exit(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitError> { if config.bootstrap.command_mode_auto_exit { info!(target: LOG_TARGET, "Auto exit argument supplied - exiting."); return Ok(()); @@ -207,7 +212,7 @@ fn wallet_or_exit(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), let mut buf = String::new(); std::io::stdin() .read_line(&mut buf) - .map_err(|e| ExitCodes::IOError(e.to_string()))?; + .map_err(|e| ExitError::new(ExitCode::IOError, e))?; match buf.as_str().trim() { "quit" | "q" | "exit" => { @@ -222,7 +227,7 @@ fn wallet_or_exit(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), } } -pub fn tui_mode(config: WalletModeConfig, mut wallet: WalletSqlite) -> Result<(), ExitCodes> { +pub fn tui_mode(config: WalletModeConfig, mut wallet: WalletSqlite) -> Result<(), ExitError> { let WalletModeConfig { base_node_config, mut base_node_selected, @@ -279,7 +284,7 @@ pub fn tui_mode(config: WalletModeConfig, mut wallet: WalletSqlite) -> Result<() Ok(()) } -pub fn recovery_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitCodes> { +pub fn recovery_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitError> { let WalletModeConfig { base_node_config, handle, @@ -318,11 +323,14 @@ pub fn recovery_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<( match wallet_mode { WalletMode::RecoveryDaemon => grpc_mode(config, wallet), WalletMode::RecoveryTui => tui_mode(config, wallet), - _ => Err(ExitCodes::RecoveryError("Unsupported post recovery mode".to_string())), + _ => Err(ExitError::new( + ExitCode::RecoveryError, + "Unsupported post recovery mode", + )), } } -pub fn grpc_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitCodes> { +pub fn grpc_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), ExitError> { let WalletModeConfig { global_config, handle, .. } = config; @@ -331,7 +339,7 @@ pub fn grpc_mode(config: WalletModeConfig, wallet: WalletSqlite) -> Result<(), E let grpc = WalletGrpcServer::new(wallet); handle .block_on(run_grpc(grpc, grpc_address)) - .map_err(ExitCodes::GrpcError)?; + .map_err(|e| ExitError::new(ExitCode::GrpcError, e))?; } else { println!("No grpc address specified"); } diff --git a/applications/tari_mining_node/src/main.rs b/applications/tari_mining_node/src/main.rs index 0b1475abfe..266deca6a1 100644 --- a/applications/tari_mining_node/src/main.rs +++ b/applications/tari_mining_node/src/main.rs @@ -40,7 +40,7 @@ use tari_app_grpc::tari_rpc::{base_node_client::BaseNodeClient, wallet_client::W use tari_app_utilities::initialization::init_configuration; use tari_common::{ configuration::bootstrap::ApplicationType, - exit_codes::{ExitCodes, ExitCodes::ConfigError}, + exit_codes::{ExitCode, ExitError}, ConfigBootstrap, DefaultConfigLoader, }; @@ -75,15 +75,16 @@ fn main() { let rt = Runtime::new().expect("Failed to start tokio runtime"); match rt.block_on(main_inner()) { Ok(_) => std::process::exit(0), - Err(exit_code) => { - eprintln!("Fatal error: {:?}", exit_code); + Err(err) => { + eprintln!("Fatal error: {:?}", err); + let exit_code = err.exit_code; error!(target: LOG_TARGET, "Exiting with code: {:?}", exit_code); - std::process::exit(exit_code.as_i32()) + std::process::exit(exit_code as i32) }, } } -async fn main_inner() -> Result<(), ExitCodes> { +async fn main_inner() -> Result<(), ExitError> { let (bootstrap, global, cfg) = init_configuration(ApplicationType::MiningNode)?; let mut config = ::load_from(&cfg).expect("Failed to load config"); config.mine_on_tip_only = global.mine_on_tip_only; @@ -108,8 +109,12 @@ async fn main_inner() -> Result<(), ExitCodes> { if !config.mining_wallet_address.is_empty() && !config.mining_pool_address.is_empty() { let url = config.mining_pool_address.clone(); let mut miner_address = config.mining_wallet_address.clone(); - let _ = RistrettoPublicKey::from_hex(&miner_address) - .map_err(|_| ConfigError("Miner is not configured with a valid wallet address.".to_string()))?; + let _ = RistrettoPublicKey::from_hex(&miner_address).map_err(|_| { + ExitError::new( + ExitCode::ConfigError, + "Miner is not configured with a valid wallet address.", + ) + })?; if !config.mining_worker_name.is_empty() { miner_address += &format!("{}{}", ".", &config.mining_worker_name); } @@ -172,7 +177,9 @@ async fn main_inner() -> Result<(), ExitCodes> { target: LOG_TARGET_FILE, "mine_on_tip_only is {}", config.mine_on_tip_only ); - let (mut node_conn, mut wallet_conn) = connect(&config).await.map_err(ExitCodes::grpc)?; + let (mut node_conn, mut wallet_conn) = connect(&config) + .await + .map_err(|err| ExitError::new(ExitCode::GrpcError, format!("GRPC connection error: {}", err)))?; let mut blocks_found: u64 = 0; loop { diff --git a/applications/tari_validator_node/src/comms.rs b/applications/tari_validator_node/src/comms.rs index 4211d5a05a..00601dbe45 100644 --- a/applications/tari_validator_node/src/comms.rs +++ b/applications/tari_validator_node/src/comms.rs @@ -28,7 +28,12 @@ use tari_app_utilities::{ identity_management::load_from_json, utilities::convert_socks_authentication, }; -use tari_common::{exit_codes::ExitCodes, CommsTransport, GlobalConfig, TorControlAuthentication}; +use tari_common::{ + exit_codes::{ExitCode, ExitError}, + CommsTransport, + GlobalConfig, + TorControlAuthentication, +}; use tari_comms::{ protocol::rpc::RpcServer, socks, @@ -61,7 +66,7 @@ pub async fn build_service_and_comms_stack( mempool: MempoolServiceHandle, db_factory: SqliteDbFactory, asset_processor: ConcreteAssetProcessor, -) -> Result<(ServiceHandles, SubscriptionFactory), ExitCodes> { +) -> Result<(ServiceHandles, SubscriptionFactory), ExitError> { // this code is duplicated from the base node let comms_config = create_comms_config(config, node_identity.clone()); @@ -71,7 +76,7 @@ pub async fn build_service_and_comms_stack( .add_initializer(P2pInitializer::new(comms_config, publisher)) .build() .await - .map_err(|err| ExitCodes::ConfigError(err.to_string()))?; + .map_err(|err| ExitError::new(ExitCode::ConfigError, err))?; let comms = handles .take_handle::() @@ -81,15 +86,15 @@ pub async fn build_service_and_comms_stack( let comms = spawn_comms_using_transport(comms, create_transport_type(config)) .await - .map_err(|e| ExitCodes::ConfigError(format!("Could not spawn using transport:{}", e)))?; + .map_err(|e| ExitError::new(ExitCode::ConfigError, format!("Could not spawn using transport:{}", e)))?; // Save final node identity after comms has initialized. This is required because the public_address can be // changed by comms during initialization when using tor. identity_management::save_as_json(&config.base_node_identity_file, &*comms.node_identity()) - .map_err(|e| ExitCodes::ConfigError(format!("Failed to save node identity: {}", e)))?; + .map_err(|e| ExitError::new(ExitCode::ConfigError, format!("Failed to save node identity: {}", e)))?; if let Some(hs) = comms.hidden_service() { identity_management::save_as_json(&config.base_node_tor_identity_file, hs.tor_identity()) - .map_err(|e| ExitCodes::ConfigError(format!("Failed to save tor identity: {}", e)))?; + .map_err(|e| ExitError::new(ExitCode::ConfigError, format!("Failed to save tor identity: {}", e)))?; } handles.register(comms); diff --git a/applications/tari_validator_node/src/dan_node.rs b/applications/tari_validator_node/src/dan_node.rs index 563e02465f..3ac9cc1380 100644 --- a/applications/tari_validator_node/src/dan_node.rs +++ b/applications/tari_validator_node/src/dan_node.rs @@ -23,7 +23,11 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; use log::info; -use tari_common::{configuration::ValidatorNodeConfig, GlobalConfig}; +use tari_common::{ + configuration::ValidatorNodeConfig, + exit_codes::{ExitCode, ExitError}, + GlobalConfig, +}; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_comms_dht::Dht; use tari_crypto::tari_utilities::hex::Hex; @@ -55,7 +59,6 @@ use crate::{ inbound_connection_service::TariCommsInboundConnectionService, outbound_connection_service::TariCommsOutboundService, }, - ExitCodes, }; const LOG_TARGET: &str = "tari::validator_node::app"; @@ -77,12 +80,12 @@ impl DanNode { db_factory: SqliteDbFactory, handles: ServiceHandles, subscription_factory: SubscriptionFactory, - ) -> Result<(), ExitCodes> { + ) -> Result<(), ExitError> { let dan_config = self .config .validator_node .as_ref() - .ok_or_else(|| ExitCodes::ConfigError("Missing dan section".to_string()))?; + .ok_or_else(|| ExitError::new(ExitCode::ConfigError, "Missing dan section"))?; let mut base_node_client = GrpcBaseNodeClient::new(dan_config.base_node_grpc_address); let mut tasks = HashMap::new(); @@ -154,14 +157,14 @@ impl DanNode { shutdown: ShutdownSignal, config: ValidatorNodeConfig, db_factory: SqliteDbFactory, - ) -> Result<(), ExitCodes> { + ) -> Result<(), ExitError> { let timeout = Duration::from_secs(asset_definition.phase_timeout); let committee = asset_definition .initial_committee .iter() .map(|s| { CommsPublicKey::from_hex(s) - .map_err(|e| ExitCodes::ConfigError(format!("could not convert to hex:{}", e))) + .map_err(|e| ExitError::new(ExitCode::ConfigError, format!("could not convert to hex:{}", e))) }) .collect::, _>>()?; @@ -216,7 +219,7 @@ impl DanNode { consensus_worker .run(shutdown.clone(), None) .await - .map_err(|err| ExitCodes::ConfigError(err.to_string()))?; + .map_err(|err| ExitError::new(ExitCode::ConfigError, err))?; Ok(()) } diff --git a/applications/tari_validator_node/src/main.rs b/applications/tari_validator_node/src/main.rs index 99e95ad21c..171cad2916 100644 --- a/applications/tari_validator_node/src/main.rs +++ b/applications/tari_validator_node/src/main.rs @@ -39,7 +39,11 @@ use futures::FutureExt; use log::*; use tari_app_grpc::tari_rpc::validator_node_server::ValidatorNodeServer; use tari_app_utilities::{identity_management::setup_node_identity, initialization::init_configuration}; -use tari_common::{configuration::bootstrap::ApplicationType, exit_codes::ExitCodes, GlobalConfig}; +use tari_common::{ + configuration::bootstrap::ApplicationType, + exit_codes::{ExitCode, ExitError}, + GlobalConfig, +}; use tari_comms::{connectivity::ConnectivityRequester, peer_manager::PeerFeatures, NodeIdentity}; use tari_comms_dht::Dht; use tari_dan_core::services::{ConcreteAssetProcessor, ConcreteAssetProxy, MempoolServiceHandle, ServiceSpecification}; @@ -61,19 +65,18 @@ const LOG_TARGET: &str = "tari::validator_node::app"; fn main() { // console_subscriber::init(); - if let Err(exit_code) = main_inner() { - eprintln!("{:?}", exit_code); + if let Err(err) = main_inner() { + let exit_code = err.exit_code; + eprintln!("{:?}", err); error!( target: LOG_TARGET, - "Exiting with code ({}): {:?}", - exit_code.as_i32(), - exit_code + "Exiting with code ({}): {:?}", exit_code as i32, exit_code ); - process::exit(exit_code.as_i32()); + process::exit(exit_code as i32); } } -fn main_inner() -> Result<(), ExitCodes> { +fn main_inner() -> Result<(), ExitError> { let (bootstrap, config, _) = init_configuration(ApplicationType::ValidatorNode)?; // let _operation_mode = cmd_args::get_operation_mode(); @@ -87,10 +90,10 @@ fn main_inner() -> Result<(), ExitCodes> { Ok(()) } -async fn run_node(config: GlobalConfig, create_id: bool) -> Result<(), ExitCodes> { +async fn run_node(config: GlobalConfig, create_id: bool) -> Result<(), ExitError> { let shutdown = Shutdown::new(); - fs::create_dir_all(&config.peer_db_path).map_err(|err| ExitCodes::ConfigError(err.to_string()))?; + fs::create_dir_all(&config.peer_db_path).map_err(|err| ExitError::new(ExitCode::ConfigError, err))?; let node_identity = setup_node_identity( &config.base_node_identity_file, &config.public_address, @@ -153,12 +156,12 @@ async fn run_node(config: GlobalConfig, create_id: bool) -> Result<(), ExitCodes Ok(()) } -fn build_runtime() -> Result { +fn build_runtime() -> Result { let mut builder = runtime::Builder::new_multi_thread(); builder .enable_all() .build() - .map_err(|e| ExitCodes::UnknownError(e.to_string())) + .map_err(|e| ExitError::new(ExitCode::UnknownError, e)) } async fn run_dan_node( @@ -169,7 +172,7 @@ async fn run_dan_node( handles: ServiceHandles, subscription_factory: SubscriptionFactory, node_identity: Arc, -) -> Result<(), ExitCodes> { +) -> Result<(), ExitError> { let node = DanNode::new(config); node.start( shutdown_signal, diff --git a/base_layer/wallet/src/error.rs b/base_layer/wallet/src/error.rs index 4145d8a08e..6f8c8e82ab 100644 --- a/base_layer/wallet/src/error.rs +++ b/base_layer/wallet/src/error.rs @@ -23,7 +23,7 @@ use diesel::result::Error as DieselError; use log::SetLoggerError; use serde_json::Error as SerdeJsonError; -use tari_common::exit_codes::ExitCodes; +use tari_common::exit_codes::{ExitCode, ExitError}; use tari_common_sqlite::error::SqliteStorageError; use tari_comms::{ connectivity::ConnectivityError, @@ -105,11 +105,11 @@ pub enum WalletError { pub const LOG_TARGET: &str = "tari::application"; -impl From for ExitCodes { +impl From for ExitError { fn from(err: WalletError) -> Self { // TODO: Log that outside log::error!(target: LOG_TARGET, "{}", err); - Self::WalletError(err.to_string()) + Self::new(ExitCode::WalletError, err) } } @@ -175,13 +175,12 @@ pub enum WalletStorageError { RecoverySeedError(String), } -impl From for ExitCodes { +impl From for ExitError { fn from(err: WalletStorageError) -> Self { use WalletStorageError::*; match err { - NoPasswordError => ExitCodes::NoPassword, - InvalidPassphrase => ExitCodes::IncorrectPassword, - e => ExitCodes::WalletError(e.to_string()), + NoPasswordError | InvalidPassphrase => ExitCode::IncorrectOrEmptyPassword.into(), + e => ExitError::new(ExitCode::WalletError, e), } } } diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 8c0390566b..3e06f26137 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use diesel::result::Error as DieselError; -use tari_common::exit_codes::ExitCodes; +use tari_common::exit_codes::{ExitCode, ExitError}; use tari_comms::{connectivity::ConnectivityError, peer_manager::node_id::NodeIdError, protocol::rpc::RpcError}; use tari_comms_dht::outbound::DhtOutboundError; use tari_core::transactions::{ @@ -176,10 +176,10 @@ pub enum OutputManagerStorageError { KeyManagerError(#[from] KeyManagerError), } -impl From for ExitCodes { +impl From for ExitError { fn from(err: OutputManagerError) -> Self { log::error!(target: crate::error::LOG_TARGET, "{}", err); - Self::WalletError(err.to_string()) + Self::new(ExitCode::WalletError, err) } } diff --git a/common/src/exit_codes.rs b/common/src/exit_codes.rs index f4792e36d9..8d6f039ff2 100644 --- a/common/src/exit_codes.rs +++ b/common/src/exit_codes.rs @@ -1,113 +1,116 @@ +use std::fmt; + use thiserror::Error; -/// Enum to show failure information #[derive(Debug, Clone, Error)] -pub enum ExitCodes { - #[error("There is an error in the configuration: {0}")] - ConfigError(String), - #[error("The application exited because an unknown error occurred: {0}. Check the logs for more details.")] - UnknownError(String), - #[error("The application exited because an interface error occurred. Check the logs for details.")] - InterfaceError, - #[error("The application exited. {0}")] - WalletError(String), - #[error("The application was not able to start the GRPC server. {0}")] - GrpcError(String), - #[error("The application did not accept the command input: {0}")] - InputError(String), - #[error("Invalid command: {0}")] - CommandError(String), - #[error("IO error: {0}")] - IOError(String), - #[error("Recovery failed: {0}")] - RecoveryError(String), - #[error("The application exited because of an internal network error: {0}")] - NetworkError(String), - #[error("The application exited because it received a message it could not interpret: {0}")] - ConversionError(String), - #[error("Your password was incorrect.")] - IncorrectPassword, - #[error("Your wallet is encrypted but no password was provided.")] - NoPassword, - #[error("The application encountered a database error: {0}")] - DatabaseError(String), - #[error("Tor connection is offline")] - TorOffline, - #[error("Database is in an inconsistent state!: {0}")] - DbInconsistentState(String), +pub struct ExitError { + pub exit_code: ExitCode, + pub details: Option, } -impl ExitCodes { - pub fn as_i32(&self) -> i32 { - match self { - Self::ConfigError(_) => 101, - Self::UnknownError(_) => 102, - Self::InterfaceError => 103, - Self::WalletError(_) => 104, - Self::GrpcError(_) => 105, - Self::InputError(_) => 106, - Self::CommandError(_) => 107, - Self::IOError(_) => 108, - Self::RecoveryError(_) => 109, - Self::NetworkError(_) => 110, - Self::ConversionError(_) => 111, - Self::IncorrectPassword | Self::NoPassword => 112, - Self::TorOffline => 113, - Self::DatabaseError(_) => 114, - Self::DbInconsistentState(_) => 115, +impl ExitError { + pub fn new(exit_code: ExitCode, details: impl ToString) -> Self { + let details = Some(details.to_string()); + Self { exit_code, details } + } +} + +impl From for ExitError { + fn from(exit_code: ExitCode) -> Self { + Self { + exit_code, + details: None, } } +} + +impl fmt::Display for ExitError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let details = self.details.as_ref().map(String::as_ref).unwrap_or(""); + write!(f, "{} {}", self.exit_code, details) + } +} - pub fn eprint_details(&self) { - use ExitCodes::*; +const TOR_HINT: &str = r#"\ +Unable to connect to the Tor control port. + +Please check that you have the Tor proxy running and \ +that access to the Tor control port is turned on. + +If you are unsure of what to do, use the following \ +command to start the Tor proxy: +tor --allow-missing-torrc --ignore-missing-torrc \ +--clientonly 1 --socksport 9050 --controlport \ +127.0.0.1:9051 --log \"warn stdout\" --clientuseipv6 1 +"#; + +impl ExitCode { + pub fn hint(&self) -> &str { + use ExitCode::*; match self { - TorOffline => { - eprintln!("Unable to connect to the Tor control port."); - eprintln!( - "Please check that you have the Tor proxy running and that access to the Tor control port is \ - turned on.", - ); - eprintln!("If you are unsure of what to do, use the following command to start the Tor proxy:"); - eprintln!( - "tor --allow-missing-torrc --ignore-missing-torrc --clientonly 1 --socksport 9050 --controlport \ - 127.0.0.1:9051 --log \"warn stdout\" --clientuseipv6 1", - ); - }, - e => { - eprintln!("{}", e); - }, + TorOffline => TOR_HINT, + _ => "", } } } -impl From for ExitCodes { +/// Enum to show failure information +#[derive(Debug, Clone, Copy, Error)] +pub enum ExitCode { + #[error("There is an error in the configuration.")] + ConfigError = 101, + #[error("The application exited because an unknown error occurred. Check the logs for more details.")] + UnknownError = 102, + #[error("The application exited because an interface error occurred. Check the logs for details.")] + InterfaceError = 103, + #[error("The application exited.")] + WalletError = 104, + #[error("The application was not able to start the GRPC server.")] + GrpcError = 105, + #[error("The application did not accept the command input.")] + InputError = 106, + #[error("Invalid command.")] + CommandError = 107, + #[error("IO error.")] + IOError = 108, + #[error("Recovery failed.")] + RecoveryError = 109, + #[error("The application exited because of an internal network error.")] + NetworkError = 110, + #[error("The application exited because it received a message it could not interpret.")] + ConversionError = 111, + #[error("Your password was incorrect or required, but not provided.")] + IncorrectOrEmptyPassword = 112, + #[error("Tor connection is offline")] + TorOffline = 113, + #[error("The application encountered a database error.")] + DatabaseError = 114, + #[error("Database is in an inconsistent state!")] + DbInconsistentState = 115, +} + +impl From for ExitError { fn from(err: super::ConfigError) -> Self { // TODO: Move it out // error!(target: LOG_TARGET, "{}", err); - Self::ConfigError(err.to_string()) + Self::new(ExitCode::ConfigError, err) } } -impl From for ExitCodes { +impl From for ExitError { fn from(err: crate::ConfigurationError) -> Self { - Self::ConfigError(err.to_string()) + Self::new(ExitCode::ConfigError, err) } } -impl From for ExitCodes { +impl From for ExitError { fn from(err: multiaddr::Error) -> Self { - Self::ConfigError(err.to_string()) + Self::new(ExitCode::ConfigError, err) } } -impl From for ExitCodes { +impl From for ExitError { fn from(err: std::io::Error) -> Self { - Self::IOError(err.to_string()) - } -} - -impl ExitCodes { - pub fn grpc(err: M) -> Self { - ExitCodes::GrpcError(format!("GRPC connection error: {}", err)) + Self::new(ExitCode::IOError, err) } } diff --git a/infrastructure/libtor/src/tor.rs b/infrastructure/libtor/src/tor.rs index a407db3283..397132060e 100644 --- a/infrastructure/libtor/src/tor.rs +++ b/infrastructure/libtor/src/tor.rs @@ -26,7 +26,11 @@ use libtor::{LogDestination, LogLevel, TorFlag}; use log::*; use multiaddr::Multiaddr; use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use tari_common::{exit_codes::ExitCodes, CommsTransport, TorControlAuthentication}; +use tari_common::{ + exit_codes::{ExitCode, ExitError}, + CommsTransport, + TorControlAuthentication, +}; use tari_shutdown::ShutdownSignal; use tempfile::{tempdir, NamedTempFile, TempDir, TempPath}; use tor_hash_passwd::EncryptedKey; @@ -74,7 +78,7 @@ impl Tor { /// Two TCP ports will be provided by the operating system. /// These ports are used for the control and socks ports, the onion address and port info are still loaded from the /// node identity file. - pub fn initialize() -> Result { + pub fn initialize() -> Result { debug!(target: LOG_TARGET, "Initializing libtor"); let mut instance = Tor::default(); @@ -108,7 +112,7 @@ impl Tor { } /// Override a given Tor comms transport with the control address and auth from this instance - pub fn update_comms_transport(&self, transport: CommsTransport) -> Result { + pub fn update_comms_transport(&self, transport: CommsTransport) -> Result { debug!(target: LOG_TARGET, "updating comms transport"); if let CommsTransport::TorHiddenService { socks_address_override, @@ -139,12 +143,12 @@ impl Tor { Ok(transport) } else { let e = format!("Expected a TorHiddenService comms transport, received: {:?}", transport); - Err(ExitCodes::ConfigError(e)) + Err(ExitError::new(ExitCode::ConfigError, e)) } } /// Run the Tor instance until the shutdown signal is received - pub async fn run(self, mut shutdown_signal: ShutdownSignal) -> Result<(), ExitCodes> { + pub async fn run(self, mut shutdown_signal: ShutdownSignal) -> Result<(), ExitError> { info!(target: LOG_TARGET, "Starting Tor instance"); let Tor { From e6e845ceb219842021f5a0b359c00079a7b7eb70 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Wed, 9 Feb 2022 17:42:51 +0200 Subject: [PATCH 03/28] fix: fix rustls and trust-dns-client after version bump (#3816) Description --- This PR just upgrades the use of the trust-dns-client and rustls to work with the latest version as pinning the specific version was not possible due to trust-dns-client itself not pinning the rustls version. How Has This Been Tested? --- updated existing tests --- Cargo.lock | 823 +++++++++++++++----------- base_layer/p2p/Cargo.toml | 4 +- base_layer/p2p/src/auto_update/dns.rs | 4 +- base_layer/p2p/src/dns/client.rs | 27 +- base_layer/p2p/src/dns/error.rs | 2 + base_layer/p2p/src/peer_seeds.rs | 4 +- 6 files changed, 501 insertions(+), 363 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bb89122659..5d000704c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,42 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "async-broadcast" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + [[package]] name = "async-io" version = "1.6.0" @@ -201,6 +237,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "async-lock" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-stream" version = "0.3.2" @@ -222,6 +278,12 @@ dependencies = [ "syn", ] +[[package]] +name = "async-task" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d306121baf53310a3fd342d88dc0824f6bbeace68347593658525565abee8" + [[package]] name = "async-trait" version = "0.1.52" @@ -268,7 +330,7 @@ dependencies = [ "log", "native-tls", "openssl", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "serde_urlencoded 0.6.1", "url 2.2.2", @@ -288,15 +350,18 @@ dependencies = [ [[package]] name = "autocfg" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "autotools" @@ -369,7 +434,7 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -443,9 +508,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882e99e4a0cb2ae6cb6e442102e8e6b7131718d94110e64c3e6a34ea9b106f37" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", "arrayvec 0.7.2", @@ -486,9 +551,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array 0.14.5", ] @@ -557,12 +622,12 @@ dependencies = [ "hyperlocal", "log", "pin-project 1.0.10", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "serde_json", "serde_urlencoded 0.7.1", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-util", "url 2.2.2", "winapi 0.3.9", @@ -575,7 +640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2f2e73fffe9455141e170fb9c1feb0ac521ec7e7dcd47a7cab72a658490fb8" dependencies = [ "chrono", - "serde 1.0.135", + "serde 1.0.136", "serde_with", ] @@ -594,7 +659,7 @@ dependencies = [ "lazy_static 1.4.0", "memchr", "regex-automata", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -633,7 +698,7 @@ checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" dependencies = [ "bincode", "byteorder", - "serde 1.0.135", + "serde 1.0.136", "trackable 0.2.24", ] @@ -661,7 +726,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -759,7 +824,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "syn", "tempfile", @@ -870,8 +935,8 @@ dependencies = [ "libc", "num-integer", "num-traits 0.2.14", - "serde 1.0.135", - "time", + "serde 1.0.136", + "time 0.1.43", "winapi 0.3.9", ] @@ -882,7 +947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6316c62053228eddd526a5e6deb6344c80bf2bc1e9786e7f90b3083e73197c1" dependencies = [ "bitstring", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -911,9 +976,9 @@ checksum = "b0fc239e0f6cb375d2402d48afb92f76f5404fd1df208a41930ec81eda078bea" [[package]] name = "clang-sys" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66045b9cb23c2e9c1520732030608b02ee07e5cfaa5a521ec15ded7fa24c90" +checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" dependencies = [ "glob", "libc", @@ -956,9 +1021,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.0.6" +version = "3.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153" +checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -1005,7 +1070,7 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "core-graphics 0.22.3", "foreign-types", "libc", @@ -1020,7 +1085,7 @@ checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "core-graphics-types", "foreign-types", "libc", @@ -1082,7 +1147,7 @@ dependencies = [ "lazy_static 1.4.0", "nom 4.2.3", "rust-ini", - "serde 1.0.135", + "serde 1.0.136", "serde-hjson 0.8.2", "serde_json", "toml 0.4.10", @@ -1098,7 +1163,7 @@ dependencies = [ "lazy_static 1.4.0", "nom 5.1.2", "rust-ini", - "serde 1.0.135", + "serde 1.0.136", "serde-hjson 0.9.1", "serde_json", "toml 0.5.8", @@ -1139,9 +1204,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys 0.8.3", "libc", @@ -1178,7 +1243,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "core-graphics-types", "foreign-types", "libc", @@ -1191,7 +1256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "foreign-types", "libc", ] @@ -1232,9 +1297,9 @@ checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" [[package]] name = "crc32fast" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if 1.0.0", ] @@ -1259,7 +1324,7 @@ dependencies = [ "rand_xoshiro", "rayon", "rayon-core", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "serde_json", "tinytemplate", @@ -1310,7 +1375,7 @@ dependencies = [ "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", ] [[package]] @@ -1329,7 +1394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", ] [[package]] @@ -1340,17 +1405,17 @@ checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", ] [[package]] name = "crossbeam-epoch" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762" +checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", "lazy_static 1.4.0", "memoffset", "scopeguard", @@ -1358,12 +1423,12 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110" +checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", ] [[package]] @@ -1378,9 +1443,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if 1.0.0", "lazy_static 1.4.0", @@ -1498,7 +1563,7 @@ dependencies = [ "csv-core", "itoa 0.4.8", "ryu", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -1558,7 +1623,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "serde 1.0.135", + "serde 1.0.136", "subtle", "zeroize", ] @@ -1573,7 +1638,7 @@ dependencies = [ "digest 0.9.0", "packed_simd_2", "rand_core 0.6.3", - "serde 1.0.135", + "serde 1.0.136", "subtle-ng", "zeroize", ] @@ -1816,7 +1881,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" dependencies = [ - "block-buffer 0.10.0", + "block-buffer 0.10.2", "crypto-common", "generic-array 0.14.5", "subtle", @@ -1885,6 +1950,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "easy-parallel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" + [[package]] name = "ed25519" version = "1.3.0" @@ -1903,7 +1974,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "rand 0.7.3", - "serde 1.0.135", + "serde 1.0.136", "sha2", "zeroize", ] @@ -1949,19 +2020,19 @@ dependencies = [ [[package]] name = "enumflags2" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +checksum = "a25c90b056b3f84111cf183cbeddef0d3a0bbe9a674f057e1a1533c315f24def" dependencies = [ "enumflags2_derive", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] name = "enumflags2_derive" -version = "0.6.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +checksum = "144ec79496cbab6f84fa125dc67be9264aef22eb8a28da8454d9c33f15108da4" dependencies = [ "proc-macro2", "quote", @@ -2044,6 +2115,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f58da196a1fc8f14cb51f373f504efc66c434c577b777c2cd30d6fad16e4822" +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + [[package]] name = "fake-simd" version = "0.1.2" @@ -2216,9 +2293,9 @@ checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" [[package]] name = "futf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" dependencies = [ "mac", "new_debug_unreachable", @@ -2232,9 +2309,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28560757fe2bb34e79f907794bb6b22ae8b0e5c669b638a1132f2592b19035b4" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -2247,9 +2324,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3dda0b6588335f360afc675d0564c17a77a2bda81ca178a4b6081bd86c7f0b" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -2257,15 +2334,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -2274,9 +2351,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9d34af5a1aac6fb380f735fe510746c38067c5bf16c7fd250280503c971b2" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-lite" @@ -2295,9 +2372,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbd947adfffb0efc70599b3ddcf7b5597bb5fa9e245eb99f62b3a5f7bb8bd3c" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ "proc-macro2", "quote", @@ -2306,21 +2383,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3055baccb68d74ff6480350f8d6eb8fcfa3aa11bdc1a1ae3afdd0514617d508" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-test" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e741bc851e1e90ad08901b329389ae77e02d5e9a0ec61955b80834630fbdc2f" +checksum = "8c3e9379dbbfb35dd6df79e895d73c0f75558827fe68eb853b858ff417a8ee98" dependencies = [ "futures-core", "futures-executor", @@ -2335,9 +2412,9 @@ dependencies = [ [[package]] name = "futures-util" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures 0.1.31", "futures-channel", @@ -2734,9 +2811,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" dependencies = [ "bytes 1.1.0", "fnv", @@ -2746,7 +2823,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-util", "tracing", ] @@ -2773,9 +2850,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" +checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" dependencies = [ "base64 0.13.0", "bitflags 1.3.2", @@ -2876,9 +2953,9 @@ checksum = "eee9694f83d9b7c09682fdb32213682939507884e5bcf227be9aff5d644b90dc" [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -2919,7 +2996,7 @@ dependencies = [ "itoa 0.4.8", "pin-project-lite 0.2.8", "socket2", - "tokio 1.15.0", + "tokio 1.16.1", "tower-service", "tracing", "want", @@ -2933,7 +3010,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite 0.2.8", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-io-timeout", ] @@ -2946,7 +3023,7 @@ dependencies = [ "bytes 1.1.0", "hyper", "native-tls", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-native-tls", ] @@ -2960,7 +3037,7 @@ dependencies = [ "hex", "hyper", "pin-project 1.0.10", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -3007,7 +3084,7 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" dependencies = [ - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", "globset", "lazy_static 1.4.0", "log", @@ -3039,7 +3116,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "hashbrown", ] @@ -3187,7 +3264,7 @@ checksum = "8eb2522ff59fbfefb955e9bd44d04d5e5c2d0e8865bfc2c3d1ab3916183ef5ee" dependencies = [ "pest", "pest_derive", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -3197,7 +3274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f8423b78fc94d12ef1a4a9d13c348c9a78766dda0cc18817adf0faf77e670c8" dependencies = [ "base64-compat", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "serde_json", ] @@ -3266,9 +3343,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.113" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" [[package]] name = "libgit2-sys" @@ -3313,9 +3390,9 @@ checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" [[package]] name = "libm" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "libsqlite3-sys" @@ -3436,9 +3513,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ "scopeguard", ] @@ -3450,7 +3527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if 1.0.0", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -3473,7 +3550,7 @@ dependencies = [ "libc", "log", "log-mdc", - "serde 1.0.135", + "serde 1.0.136", "serde-value 0.5.3", "serde_derive", "serde_yaml", @@ -3499,7 +3576,7 @@ dependencies = [ "log-mdc", "parking_lot 0.11.2", "regex", - "serde 1.0.135", + "serde 1.0.136", "serde-value 0.7.0", "serde_json", "serde_yaml", @@ -3518,10 +3595,10 @@ dependencies = [ "cfg-if 1.0.0", "generator", "scoped-tls", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tracing", - "tracing-subscriber 0.3.6", + "tracing-subscriber 0.3.8", ] [[package]] @@ -3618,7 +3695,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] @@ -3692,7 +3769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] @@ -3759,7 +3836,7 @@ dependencies = [ "fixed-hash", "hex", "hex-literal", - "serde 1.0.135", + "serde 1.0.136", "serde-big-array", "thiserror", "tiny-keccak", @@ -3777,7 +3854,7 @@ dependencies = [ "data-encoding", "multihash", "percent-encoding 2.1.0", - "serde 1.0.135", + "serde 1.0.136", "static_assertions", "unsigned-varint", "url 2.2.2", @@ -3832,16 +3909,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nb-connect" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bb540dc6ef51cfe1916ec038ce7a620daf3a111e2502d745197cd53d6bca15" -dependencies = [ - "libc", - "socket2", -] - [[package]] name = "ndk" version = "0.4.0" @@ -3920,19 +3987,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "nix" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if 0.1.10", - "libc", - "void", -] - [[package]] name = "nix" version = "0.23.1" @@ -3981,12 +4035,12 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.5.5" +version = "4.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ebab865e67efdd7182a88d76cadbdd2a8d02d1c7a4e16bb7c234016a12cac" +checksum = "367e1355a950d3e758e414f3ca1b3981a57a2aa1fa3338eb0059f5b230b6ffa4" dependencies = [ "mac-notification-sys", - "serde 1.0.135", + "serde 1.0.136", "winrt-notification", "zbus", "zvariant", @@ -4008,7 +4062,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits 0.2.14", ] @@ -4019,15 +4073,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d51546d704f52ef14b3c962b5776e53d5b862e5790e40a350d366c209bd7f7a" dependencies = [ - "autocfg 0.1.7", + "autocfg 0.1.8", "byteorder", "lazy_static 1.4.0", - "libm 0.2.1", + "libm 0.2.2", "num-integer", "num-iter", "num-traits 0.2.14", "rand 0.7.3", - "serde 1.0.135", + "serde 1.0.136", "smallvec", "zeroize", ] @@ -4059,7 +4113,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-traits 0.2.14", ] @@ -4069,7 +4123,7 @@ version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits 0.2.14", ] @@ -4080,7 +4134,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "num-integer", "num-traits 0.2.14", ] @@ -4100,7 +4154,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", ] [[package]] @@ -4134,6 +4188,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -4226,7 +4289,7 @@ version = "0.9.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "cc", "libc", "openssl-src", @@ -4242,14 +4305,14 @@ checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" dependencies = [ "async-trait", "crossbeam-channel 0.5.2", - "futures 0.3.19", + "futures 0.3.21", "js-sys", "lazy_static 1.4.0", "percent-encoding 2.1.0", "pin-project 1.0.10", "rand 0.8.4", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", ] @@ -4283,7 +4346,7 @@ dependencies = [ "reqwest", "thiserror", "thrift", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -4313,14 +4376,24 @@ dependencies = [ "num-traits 0.2.14", ] +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite 0.2.8", +] + [[package]] name = "os_info" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198e392be7e882f0c2836f425e430f81d9a0e99651e4646311347417cddbfd43" +checksum = "023df84d545ef479cf67fd2f4459a613585c9db4852c2fad12ab70587859d340" dependencies = [ "log", - "serde 1.0.135", + "serde 1.0.136", "winapi 0.3.9", ] @@ -4342,11 +4415,11 @@ checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" [[package]] name = "packed_simd_2" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c0c06716cfc81616fa8e22b721ce92fecd594508bc0eb3d04ae3ef35ac10c5" +checksum = "defdcfef86dcc44ad208f71d9ff4ce28df6537a4e0d6b0e8e845cb8ca10059a6" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libm 0.1.4", ] @@ -4398,7 +4471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api 0.4.5", + "lock_api 0.4.6", "parking_lot_core 0.8.5", ] @@ -4979,9 +5052,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.25.2" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" +checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" [[package]] name = "qrcode" @@ -5195,7 +5268,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" dependencies = [ - "autocfg 1.0.1", + "autocfg 1.1.0", "crossbeam-deque", "either", "rayon-core", @@ -5209,7 +5282,7 @@ checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" dependencies = [ "crossbeam-channel 0.5.2", "crossbeam-deque", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", "lazy_static 1.4.0", "num_cpus", ] @@ -5318,10 +5391,10 @@ dependencies = [ "native-tls", "percent-encoding 2.1.0", "pin-project-lite 0.2.8", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "serde_urlencoded 0.7.1", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-native-tls", "url 2.2.2", "wasm-bindgen", @@ -5397,7 +5470,7 @@ checksum = "011e1d58446e9fa3af7cdc1fb91295b10621d3ac4cb3a85cc86385ee9ca50cd3" dependencies = [ "byteorder", "rmp", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -5441,7 +5514,7 @@ dependencies = [ "base64 0.13.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils 0.8.6", + "crossbeam-utils 0.8.7", ] [[package]] @@ -5477,7 +5550,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.4", + "semver 1.0.5", ] [[package]] @@ -5496,15 +5569,23 @@ dependencies = [ [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" dependencies = [ - "base64 0.13.0", "log", "ring", "sct", - "webpki", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64 0.13.0", ] [[package]] @@ -5527,7 +5608,7 @@ dependencies = [ "libc", "log", "memchr", - "nix 0.23.1", + "nix", "radix_trie", "scopeguard", "smallvec", @@ -5601,9 +5682,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -5611,12 +5692,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09d3c15d814eda1d6a836f2f2b56a6abc1446c8a34351cb3180d3db92ffe4ce" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "core-foundation-sys 0.8.3", "libc", "security-framework-sys", @@ -5624,9 +5705,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90dd10c41c6bfc633da6e0c659bd25d31e0791e5974ac42970267d59eba87f7" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys 0.8.3", "libc", @@ -5663,9 +5744,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" [[package]] name = "semver-parser" @@ -5684,9 +5765,9 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" [[package]] name = "serde" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] @@ -5697,7 +5778,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18b20e7752957bbe9661cff4e0bb04d183d0948cdab2ea58cdb9df36a61dfe62" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", "serde_derive", ] @@ -5733,7 +5814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a663f873dedc4eac1a559d4c6bc0d0b2c34dc5ac4702e105014b8281489e44f" dependencies = [ "ordered-float 1.1.1", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -5743,14 +5824,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ "ordered-float 2.10.0", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] name = "serde_derive" -version = "1.0.135" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -5765,7 +5846,7 @@ checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" dependencies = [ "itoa 1.0.1", "ryu", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -5796,7 +5877,7 @@ checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" dependencies = [ "dtoa", "itoa 0.4.8", - "serde 1.0.135", + "serde 1.0.136", "url 2.2.2", ] @@ -5809,17 +5890,17 @@ dependencies = [ "form_urlencoded", "itoa 1.0.1", "ryu", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] name = "serde_with" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6056b4cb69b6e43e3a0f055def223380baecc99da683884f205bf347f7c4b3" +checksum = "ec1e6ec4d8950e5b1e894eac0d360742f3b1407a6078a604a731c4b3f49cefbc" dependencies = [ "rustversion", - "serde 1.0.135", + "serde 1.0.136", "serde_with_macros", ] @@ -5843,7 +5924,7 @@ checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" dependencies = [ "indexmap", "ryu", - "serde 1.0.135", + "serde 1.0.136", "yaml-rust", ] @@ -6033,9 +6114,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f82496b90c36d70af5fcd482edaa2e0bd16fade569de1330405fecbbdac736b" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi 0.3.9", @@ -6097,16 +6178,16 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" [[package]] name = "string_cache" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" +checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" dependencies = [ "lazy_static 1.4.0", "new_debug_unreachable", "parking_lot 0.11.2", - "phf_shared 0.8.0", + "phf_shared 0.10.0", "precomputed-hash", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -6323,7 +6404,7 @@ dependencies = [ "cairo-rs", "cc", "cocoa", - "core-foundation 0.9.2", + "core-foundation 0.9.3", "core-graphics 0.22.3", "core-video-sys", "crossbeam-channel 0.5.2", @@ -6346,7 +6427,7 @@ dependencies = [ "parking_lot 0.11.2", "raw-window-handle 0.3.4", "scopeguard", - "serde 1.0.135", + "serde 1.0.136", "unicode-segmentation", "winapi 0.3.9", "x11-dl", @@ -6385,11 +6466,11 @@ version = "0.27.3" dependencies = [ "config 0.9.3", "dirs-next 1.0.2", - "futures 0.3.19", + "futures 0.3.21", "json5", "log", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "structopt", "tari_common", "tari_common_types", @@ -6398,7 +6479,7 @@ dependencies = [ "tari_p2p", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -6410,7 +6491,7 @@ dependencies = [ "chrono", "config 0.9.3", "either", - "futures 0.3.19", + "futures 0.3.21", "log", "log-mdc", "num_cpus", @@ -6437,7 +6518,7 @@ dependencies = [ "tari_shutdown", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", "tracing", "tracing-opentelemetry", @@ -6457,7 +6538,7 @@ dependencies = [ "merlin", "rand 0.8.4", "rand_core 0.6.3", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "sha3", "subtle-ng", @@ -6471,12 +6552,12 @@ dependencies = [ "blake2", "diesel", "diesel_migrations", - "futures 0.3.19", + "futures 0.3.21", "log", "prost", "prost-types", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "structopt", "tari_app_grpc", @@ -6491,7 +6572,7 @@ dependencies = [ "tauri", "tauri-build", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", "uuid", ] @@ -6511,7 +6592,7 @@ dependencies = [ "multiaddr", "path-clean", "prost-build", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sha2", "structopt", @@ -6538,11 +6619,11 @@ dependencies = [ "digest 0.9.0", "lazy_static 1.4.0", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "tari_crypto", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -6560,7 +6641,7 @@ dependencies = [ "data-encoding", "digest 0.9.0", "env_logger 0.7.1", - "futures 0.3.19", + "futures 0.3.21", "lazy_static 1.4.0", "lmdb-zero", "log", @@ -6573,7 +6654,7 @@ dependencies = [ "prost", "prost-types", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "serde_json", "snow", @@ -6586,7 +6667,7 @@ dependencies = [ "tari_test_utils 0.27.3", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tokio-util", "tower", @@ -6608,7 +6689,7 @@ dependencies = [ "diesel_migrations", "digest 0.9.0", "env_logger 0.7.1", - "futures 0.3.19", + "futures 0.3.21", "futures-test", "futures-util", "lazy_static 1.4.0", @@ -6621,7 +6702,7 @@ dependencies = [ "prost", "prost-types", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "tari_common", "tari_common_sqlite", @@ -6634,7 +6715,7 @@ dependencies = [ "tari_utilities", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tower", ] @@ -6643,14 +6724,14 @@ dependencies = [ name = "tari_comms_rpc_macros" version = "0.27.3" dependencies = [ - "futures 0.3.19", + "futures 0.3.21", "proc-macro2", "prost", "quote", "syn", "tari_comms", "tari_test_utils 0.27.3", - "tokio 1.15.0", + "tokio 1.16.1", "tower-service", ] @@ -6662,7 +6743,7 @@ dependencies = [ "chrono", "crossterm 0.17.7", "digest 0.9.0", - "futures 0.3.19", + "futures 0.3.21", "log", "opentelemetry", "opentelemetry-jaeger", @@ -6689,7 +6770,7 @@ dependencies = [ "tari_utilities", "tari_wallet", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", "tracing", "tracing-opentelemetry", @@ -6716,7 +6797,7 @@ dependencies = [ "digest 0.9.0", "env_logger 0.7.1", "fs2 0.3.0", - "futures 0.3.19", + "futures 0.3.21", "hex", "integer-encoding 3.0.2", "lmdb-zero", @@ -6730,7 +6811,7 @@ dependencies = [ "prost-types", "rand 0.8.4", "randomx-rs", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sha3", "strum_macros 0.22.0", @@ -6750,7 +6831,7 @@ dependencies = [ "tari_utilities", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tracing", "tracing-attributes", "uint", @@ -6771,7 +6852,7 @@ dependencies = [ "merlin", "rand 0.8.4", "rmp-serde", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sha2", "sha3", @@ -6799,13 +6880,13 @@ dependencies = [ "bytecodec", "clap 2.34.0", "digest 0.9.0", - "futures 0.3.19", + "futures 0.3.21", "lmdb-zero", "log", "patricia_tree", "prost", "prost-types", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tari_common", "tari_common_types", @@ -6823,7 +6904,7 @@ dependencies = [ "tari_test_utils 0.8.1", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", ] @@ -6842,7 +6923,7 @@ dependencies = [ "tari_dan_core", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", ] @@ -6862,7 +6943,7 @@ dependencies = [ "getrandom 0.2.4", "js-sys", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "serde_json", "sha2", @@ -6882,10 +6963,10 @@ dependencies = [ "bollard", "config 0.11.0", "env_logger 0.9.0", - "futures 0.3.19", + "futures 0.3.21", "log", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "strum 0.23.0", "strum_macros 0.23.1", @@ -6894,7 +6975,7 @@ dependencies = [ "tauri", "tauri-build", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tor-hash-passwd", ] @@ -6923,14 +7004,14 @@ dependencies = [ "chrono", "config 0.9.3", "env_logger 0.7.1", - "futures 0.3.19", + "futures 0.3.21", "hex", "hyper", "jsonrpc", "log", "rand 0.8.4", "reqwest", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "structopt", "tari_app_grpc", @@ -6941,7 +7022,7 @@ dependencies = [ "tari_crypto", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", "tracing", "url 2.2.2", @@ -6952,13 +7033,13 @@ name = "tari_metrics" version = "0.1.0" dependencies = [ "anyhow", - "futures 0.3.19", + "futures 0.3.21", "log", "once_cell", "prometheus", "reqwest", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "warp", ] @@ -6969,7 +7050,7 @@ dependencies = [ "bufstream", "chrono", "crossbeam", - "futures 0.3.19", + "futures 0.3.21", "hex", "log", "native-tls", @@ -6977,7 +7058,7 @@ dependencies = [ "prost-types", "rand 0.8.4", "reqwest", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sha3", "tari_app_grpc", @@ -6988,7 +7069,7 @@ dependencies = [ "tari_crypto", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", ] @@ -7003,7 +7084,7 @@ dependencies = [ "digest 0.9.0", "log", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tari_crypto", "tari_utilities", @@ -7019,7 +7100,7 @@ dependencies = [ "chrono", "clap 2.34.0", "fs2 0.3.0", - "futures 0.3.19", + "futures 0.3.21", "lazy_static 1.4.0", "lmdb-zero", "log", @@ -7029,8 +7110,8 @@ dependencies = [ "rand 0.8.4", "reqwest", "rustls", - "semver 1.0.4", - "serde 1.0.135", + "semver 1.0.5", + "serde 1.0.136", "serde_derive", "tari_common", "tari_comms", @@ -7043,12 +7124,12 @@ dependencies = [ "tari_utilities", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tower", "tower-service", "trust-dns-client", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -7057,13 +7138,13 @@ version = "0.27.3" dependencies = [ "anyhow", "async-trait", - "futures 0.3.19", + "futures 0.3.21", "futures-test", "log", "tari_shutdown", "tari_test_utils 0.27.3", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tower", "tower-service", ] @@ -7072,8 +7153,8 @@ dependencies = [ name = "tari_shutdown" version = "0.27.3" dependencies = [ - "futures 0.3.19", - "tokio 1.15.0", + "futures 0.3.21", + "tokio 1.16.1", ] [[package]] @@ -7084,7 +7165,7 @@ dependencies = [ "lmdb-zero", "log", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_derive", "tari_utilities", "thiserror", @@ -7096,7 +7177,7 @@ version = "0.27.3" dependencies = [ "hex", "libc", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tari_common", "tari_comms", @@ -7115,14 +7196,14 @@ dependencies = [ "chrono", "config 0.9.3", "env_logger 0.7.1", - "futures 0.3.19", + "futures 0.3.21", "hex", "hyper", "jsonrpc", "log", "rand 0.7.3", "reqwest", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "structopt", "tari_app_grpc", @@ -7132,7 +7213,7 @@ dependencies = [ "tari_crypto", "tari_utilities", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tonic", "tonic-build", "tracing", @@ -7145,7 +7226,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f11c0804a3f136ad0f821981411215886f0039bf1519c4ec0ec0af8098a8def" dependencies = [ - "futures 0.3.19", + "futures 0.3.21", "futures-test", "lazy_static 1.4.0", "rand 0.7.3", @@ -7157,12 +7238,12 @@ dependencies = [ name = "tari_test_utils" version = "0.27.3" dependencies = [ - "futures 0.3.19", + "futures 0.3.21", "futures-test", "rand 0.8.4", "tari_shutdown", "tempfile", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -7178,7 +7259,7 @@ dependencies = [ "clear_on_drop", "newtype-ops", "rand 0.7.3", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "thiserror", ] @@ -7193,13 +7274,13 @@ dependencies = [ "bytecodec", "clap 2.34.0", "digest 0.9.0", - "futures 0.3.19", + "futures 0.3.21", "lmdb-zero", "log", "patricia_tree", "prost", "prost-types", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tari_app_grpc", "tari_app_utilities", @@ -7220,7 +7301,7 @@ dependencies = [ "tari_storage", "tari_test_utils 0.27.3", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tonic", ] @@ -7242,14 +7323,14 @@ dependencies = [ "digest 0.9.0", "env_logger 0.7.1", "fs2 0.3.0", - "futures 0.3.19", + "futures 0.3.21", "libsqlite3-sys", "lmdb-zero", "log", "log4rs 1.0.0", "prost", "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sha2", "strum 0.22.0", @@ -7270,7 +7351,7 @@ dependencies = [ "tari_utilities", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "tower", ] @@ -7279,7 +7360,7 @@ name = "tari_wallet_ffi" version = "0.27.3" dependencies = [ "chrono", - "futures 0.3.19", + "futures 0.3.21", "lazy_static 1.4.0", "libc", "log", @@ -7301,7 +7382,7 @@ dependencies = [ "tari_wallet", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -7319,7 +7400,7 @@ dependencies = [ "either", "embed_plist", "flate2", - "futures 0.3.19", + "futures 0.3.21", "futures-lite", "glib", "gtk", @@ -7335,8 +7416,8 @@ dependencies = [ "rand 0.8.4", "raw-window-handle 0.3.4", "rfd", - "semver 1.0.4", - "serde 1.0.135", + "semver 1.0.5", + "serde 1.0.136", "serde_json", "serde_repr", "shared_child", @@ -7348,7 +7429,7 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", - "tokio 1.15.0", + "tokio 1.16.1", "url 2.2.2", "uuid", "zip", @@ -7379,7 +7460,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tauri-utils", "thiserror", @@ -7409,7 +7490,7 @@ dependencies = [ "http", "http-range", "infer", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tauri-utils", "thiserror", @@ -7445,7 +7526,7 @@ dependencies = [ "phf 0.10.1", "proc-macro2", "quote", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "thiserror", "url 2.2.2", @@ -7491,13 +7572,13 @@ name = "test_faucet" version = "0.27.3" dependencies = [ "rand 0.8.4", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tari_common_types", "tari_core", "tari_crypto", "tari_utilities", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -7596,6 +7677,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "time" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +dependencies = [ + "libc", + "num_threads", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -7611,7 +7702,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", "serde_json", ] @@ -7646,9 +7737,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" +checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a" dependencies = [ "bytes 1.1.0", "libc", @@ -7669,7 +7760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite 0.2.8", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -7690,18 +7781,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" dependencies = [ "rustls", - "tokio 1.15.0", - "webpki", + "tokio 1.16.1", + "webpki 0.22.0", ] [[package]] @@ -7712,7 +7803,7 @@ checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite 0.2.8", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-util", ] @@ -7728,7 +7819,7 @@ dependencies = [ "futures-sink", "log", "pin-project-lite 0.2.8", - "tokio 1.15.0", + "tokio 1.16.1", ] [[package]] @@ -7737,7 +7828,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -7746,7 +7837,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -7770,7 +7861,7 @@ dependencies = [ "pin-project 1.0.10", "prost", "prost-derive", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tokio-util", "tower", @@ -7819,7 +7910,7 @@ dependencies = [ "pin-project-lite 0.2.8", "rand 0.8.4", "slab", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tokio-util", "tower-layer", @@ -7841,9 +7932,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9" dependencies = [ "cfg-if 1.0.0", "log", @@ -7854,9 +7945,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" dependencies = [ "proc-macro2", "quote", @@ -7865,11 +7956,12 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" dependencies = [ "lazy_static 1.4.0", + "valuable", ] [[package]] @@ -7908,11 +8000,11 @@ dependencies = [ [[package]] name = "tracing-serde" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ - "serde 1.0.135", + "serde 1.0.136", "tracing-core", ] @@ -7927,7 +8019,7 @@ dependencies = [ "lazy_static 1.4.0", "matchers 0.0.1", "regex", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "sharded-slab", "smallvec", @@ -7940,9 +8032,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77be66445c4eeebb934a7340f227bfe7b338173d3f8c00a60a5a58005c9faecf" +checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa" dependencies = [ "ansi_term", "lazy_static 1.4.0", @@ -7993,12 +8085,11 @@ checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" [[package]] name = "trust-dns-client" -version = "0.21.0-alpha.4" +version = "0.21.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bd50900d7796338807398681ed6d8de0c840d0d9aac65cff093b5635b22e57" +checksum = "b62dfcea87b25f0810e2a527458dd621e252fd8a5827153329308d6e1f252d68" dependencies = [ "cfg-if 1.0.0", - "chrono", "data-encoding", "futures-channel", "futures-util", @@ -8009,16 +8100,17 @@ dependencies = [ "ring", "rustls", "thiserror", - "tokio 1.15.0", + "time 0.3.7", + "tokio 1.16.1", "trust-dns-proto", - "webpki", + "webpki 0.22.0", ] [[package]] name = "trust-dns-proto" -version = "0.21.0-alpha.4" +version = "0.21.0-alpha.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e43c627e8301d45629cdfaf63e2e0c57305a07e9f1ea48208fd262ba2d87eb" +checksum = "df4689a56fb36e79b76d13f52056c116f1f014afb1bd330162f2d9dc08ef5405" dependencies = [ "async-trait", "cfg-if 1.0.0", @@ -8034,13 +8126,14 @@ dependencies = [ "rand 0.8.4", "ring", "rustls", + "rustls-pemfile", "smallvec", "thiserror", "tinyvec", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-rustls", "url 2.2.2", - "webpki", + "webpki 0.22.0", ] [[package]] @@ -8105,9 +8198,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uint" -version = "0.9.1" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" dependencies = [ "byteorder", "crunchy", @@ -8141,9 +8234,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -8209,7 +8302,7 @@ dependencies = [ "idna 0.2.3", "matches", "percent-encoding 2.1.0", - "serde 1.0.135", + "serde 1.0.136", ] [[package]] @@ -8231,9 +8324,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom 0.2.4", - "serde 1.0.135", + "serde 1.0.136", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -8270,12 +8369,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "waker-fn" version = "1.1.0" @@ -8321,10 +8414,10 @@ dependencies = [ "percent-encoding 2.1.0", "pin-project 1.0.10", "scoped-tls", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "serde_urlencoded 0.7.1", - "tokio 1.15.0", + "tokio 1.16.1", "tokio-stream", "tokio-util", "tower-service", @@ -8350,7 +8443,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ "cfg-if 1.0.0", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "wasm-bindgen-macro", ] @@ -8501,6 +8594,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webview2" version = "0.1.4" @@ -8734,7 +8837,7 @@ dependencies = [ "objc", "objc_id", "once_cell", - "serde 1.0.135", + "serde 1.0.136", "serde_json", "tao", "thiserror", @@ -8808,7 +8911,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7d9028f208dd5e63c614be69f115c1b53cacc1111437d4c765185856666c107" dependencies = [ - "futures 0.3.19", + "futures 0.3.21", "log", "nohash-hasher", "parking_lot 0.11.2", @@ -8818,39 +8921,65 @@ dependencies = [ [[package]] name = "zbus" -version = "1.9.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2326acc379a3ac4e34b794089f5bdb17086bf29a5fdf619b7b4cc772dc2e9dad" +checksum = "7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a" dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", "byteorder", "derivative", "enumflags2", - "fastrand", - "futures 0.3.19", - "nb-connect", - "nix 0.17.0", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static 1.4.0", + "nix", "once_cell", - "polling", - "scoped-tls", - "serde 1.0.135", + "ordered-stream", + "rand 0.8.4", + "serde 1.0.136", "serde_repr", + "sha1", + "static_assertions", + "winapi 0.3.9", "zbus_macros", + "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" -version = "1.9.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a482c56029e48681b89b92b5db3c446db0915e8dd1052c0328a574eda38d5f93" +checksum = "36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040" dependencies = [ - "proc-macro-crate 0.1.5", + "proc-macro-crate 1.1.0", "proc-macro2", "quote", + "regex", "syn", ] +[[package]] +name = "zbus_names" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1" +dependencies = [ + "serde 1.0.136", + "static_assertions", + "zvariant", +] + [[package]] name = "zeroize" version = "1.3.0" @@ -8883,7 +9012,7 @@ dependencies = [ "crc32fast", "flate2", "thiserror", - "time", + "time 0.1.43", ] [[package]] @@ -8917,23 +9046,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "2.10.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68c7b55f2074489b7e8e07d2d0a6ee6b4f233867a653c664d8020ba53692525" +checksum = "49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.135", + "serde 1.0.136", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "2.10.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ca5e22593eb4212382d60d26350065bf2a02c34b85bc850474a74b589a3de9" +checksum = "8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a" dependencies = [ "proc-macro-crate 1.1.0", "proc-macro2", diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index 900913dfb8..2b7f079bd6 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -38,8 +38,8 @@ tokio = { version = "1.11", features = ["macros"] } tokio-stream = { version = "0.1.7", default-features = false, features = ["time"] } tower = "0.4.11" tower-service = { version = "0.3.1" } -trust-dns-client = { version = "0.21.0-alpha.4", features = ["dns-over-rustls"] } -rustls = "0.19.1" +trust-dns-client = { version = "=0.21.0-alpha.5", features = ["dns-over-rustls"] } +rustls = "0.20.2" webpki = "0.21" [dev-dependencies] diff --git a/base_layer/p2p/src/auto_update/dns.rs b/base_layer/p2p/src/auto_update/dns.rs index d75e09c55c..e3a7cd3325 100644 --- a/base_layer/p2p/src/auto_update/dns.rs +++ b/base_layer/p2p/src/auto_update/dns.rs @@ -208,9 +208,9 @@ mod test { let mut record = Record::new(); record .set_record_type(RecordType::TXT) - .set_rdata(RData::TXT(rdata::TXT::new( + .set_data(Some(RData::TXT(rdata::TXT::new( contents.into_iter().map(ToString::to_string).collect(), - ))); + )))); mock::message(resp_query, vec![record], vec![], vec![]).into() } diff --git a/base_layer/p2p/src/dns/client.rs b/base_layer/p2p/src/dns/client.rs index e9cdcb7f92..462f3afed8 100644 --- a/base_layer/p2p/src/dns/client.rs +++ b/base_layer/p2p/src/dns/client.rs @@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration}; use futures::{future, FutureExt}; -use rustls::{ClientConfig, ProtocolVersion, RootCertStore}; +use rustls::{ClientConfig, RootCertStore}; use tari_common::DnsNameServer; use tari_shutdown::Shutdown; use tokio::task; @@ -96,12 +96,18 @@ impl DnsClient { .answers() .iter() .map(|answer| { - let data = answer.rdata(); + let data = if let Some(d) = answer.data() { + d + } else { + return Err(DnsClientError::NoRecordDataPresent); + }; let mut buf = Vec::new(); let mut decoder = BinEncoder::new(&mut buf); data.emit(&mut decoder).unwrap(); - buf + Ok(buf) }) + .collect::>, DnsClientError>>()? + .iter() .filter_map(|txt| { if txt.is_empty() { return None; @@ -185,13 +191,14 @@ where C: DnsHandle fn default_client_config() -> Arc { let mut root_store = RootCertStore::empty(); - root_store.add_server_trust_anchors(&roots::TLS_SERVER_ROOTS); - let versions = vec![ProtocolVersion::TLSv1_2]; - - let mut client_config = ClientConfig::new(); - client_config.root_store = root_store; - client_config.versions = versions; - client_config.alpn_protocols.push("h2".as_bytes().to_vec()); + root_store.add_server_trust_anchors(roots::TLS_SERVER_ROOTS.0.iter().map(|ta| { + rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(ta.subject, ta.spki, ta.name_constraints) + })); + + let client_config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); Arc::new(client_config) } diff --git a/base_layer/p2p/src/dns/error.rs b/base_layer/p2p/src/dns/error.rs index c49a8d81c4..4e8ebb4c45 100644 --- a/base_layer/p2p/src/dns/error.rs +++ b/base_layer/p2p/src/dns/error.rs @@ -32,4 +32,6 @@ pub enum DnsClientError { Timeout, #[error("Failed to parse name server string")] NameServerParseFailed, + #[error("No record data present")] + NoRecordDataPresent, } diff --git a/base_layer/p2p/src/peer_seeds.rs b/base_layer/p2p/src/peer_seeds.rs index 8e628a9872..3d2652b02e 100644 --- a/base_layer/p2p/src/peer_seeds.rs +++ b/base_layer/p2p/src/peer_seeds.rs @@ -218,9 +218,9 @@ mod test { let mut record = Record::new(); record .set_record_type(RecordType::TXT) - .set_rdata(RData::TXT(rdata::TXT::new( + .set_data(Some(RData::TXT(rdata::TXT::new( contents.into_iter().map(ToString::to_string).collect(), - ))); + )))); mock::message(resp_query, vec![record], vec![], vec![]).into() } From 5453c9e05b7d35b7586ff9375ba30ed7ecc7a9dd Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Wed, 9 Feb 2022 19:02:58 +0200 Subject: [PATCH 04/28] feat!: add scanned transaction handling for one-sided payments with callbacks (#3794) Description --- - Added separate statuses for scanned one-sided transactions to enable unique events and validation. - Added validation for one-sided payments updating their mined height and confirmations. - Added callbacks for scanned one-sided transactions (unconfirmed and confirmed) - Updated the wallet FFI with the callbacks. Motivation and Context --- Currently when a one-sided output is found by scanning the blockchain a Faux transaction is created for it with the Imported status, the same as for a manually imported output. Outputs found by scanning (for one-sided payments) should be handled differently to a manual import and their mined height and number confirmations should be validated. How Has This Been Tested? --- - Unit tests - Cucumber tests - System level testing --- applications/ffi_client/index.js | 14 ++ applications/ffi_client/lib/index.js | 3 + applications/ffi_client/recovery.js | 12 ++ applications/tari_app_grpc/proto/wallet.proto | 4 + .../src/conversions/transaction.rs | 2 + .../src/ui/state/wallet_event_monitor.rs | 9 +- base_layer/common_types/src/transaction.rs | 62 ++++++- .../src/output_manager_service/handle.rs | 52 ++++-- .../recovery/standard_outputs_recoverer.rs | 8 +- .../src/output_manager_service/service.rs | 45 +++-- .../wallet/src/transaction_service/handle.rs | 73 ++++++-- .../protocols/transaction_receive_protocol.rs | 1 + .../protocols/transaction_send_protocol.rs | 1 + .../transaction_validation_protocol.rs | 14 +- .../wallet/src/transaction_service/service.rs | 82 +++++++-- .../transaction_service/storage/database.rs | 40 ++++- .../src/transaction_service/storage/models.rs | 3 +- .../transaction_service/storage/sqlite_db.rs | 67 ++++++- .../tasks/check_faux_transaction_status.rs | 167 ++++++++++++++++++ .../check_imported_transaction_status.rs | 78 -------- .../src/transaction_service/tasks/mod.rs | 2 +- .../utxo_scanner_service/utxo_scanner_task.rs | 68 +++++-- base_layer/wallet/src/wallet.rs | 23 ++- .../output_manager_service_tests/service.rs | 6 +- .../support/output_manager_service_mock.rs | 10 +- .../tests/support/transaction_service_mock.rs | 4 +- .../transaction_service_tests/service.rs | 130 ++++++++++---- .../transaction_service_tests/storage.rs | 81 ++++++++- .../transaction_protocols.rs | 1 + base_layer/wallet/tests/wallet.rs | 25 +-- base_layer/wallet_ffi/src/callback_handler.rs | 64 ++++++- .../wallet_ffi/src/callback_handler_tests.rs | 149 ++++++++++++++-- base_layer/wallet_ffi/src/lib.rs | 76 +++++++- base_layer/wallet_ffi/wallet.h | 63 ++++--- integration_tests/features/WalletFFI.feature | 47 +++-- .../features/support/ffi_steps.js | 60 +++++-- .../features/support/wallet_steps.js | 4 +- integration_tests/helpers/ffi/ffiInterface.js | 14 ++ integration_tests/helpers/ffi/wallet.js | 99 ++++++++--- integration_tests/helpers/walletFFIClient.js | 2 +- integration_tests/package-lock.json | 128 +++++--------- 41 files changed, 1386 insertions(+), 407 deletions(-) create mode 100644 base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs delete mode 100644 base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs diff --git a/applications/ffi_client/index.js b/applications/ffi_client/index.js index 20aeaf6c39..c7a7c81f39 100644 --- a/applications/ffi_client/index.js +++ b/applications/ffi_client/index.js @@ -69,6 +69,18 @@ try { console.log("txMinedUnconfirmed: ", ptr, confirmations); } ); + // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txFauxConfirmed: ", ptr); + }); + // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + const txFauxUnconfirmed = ffi.Callback( + "void", + ["pointer"], + function (ptr, confirmations) { + console.log("txFauxUnconfirmed: ", ptr, confirmations); + } + ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { console.log("directSendResult: ", i, j); @@ -112,6 +124,8 @@ try { txBroadcast, txMined, txMinedUnconfirmed, + txFauxConfirmed, + txFauxUnconfirmed, directSendResult, safResult, txCancelled, diff --git a/applications/ffi_client/lib/index.js b/applications/ffi_client/lib/index.js index db922de1f0..b9e8b34da7 100644 --- a/applications/ffi_client/lib/index.js +++ b/applications/ffi_client/lib/index.js @@ -66,6 +66,9 @@ const libWallet = ffi.Library("./libtari_wallet_ffi.dylib", { fn, fn, fn, + fn, + fn, + bool, errPtr, ], ], diff --git a/applications/ffi_client/recovery.js b/applications/ffi_client/recovery.js index 0c51d3daa3..bf82225ca2 100644 --- a/applications/ffi_client/recovery.js +++ b/applications/ffi_client/recovery.js @@ -80,6 +80,18 @@ try { console.log("txMinedUnconfirmed: ", ptr, confirmations); } ); + // callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + const txFauxConfirmed = ffi.Callback("void", ["pointer"], function (ptr) { + console.log("txFauxConfirmed: ", ptr); + }); + // callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + const txFauxUnconfirmed = ffi.Callback( + "void", + ["pointer"], + function (ptr, confirmations) { + console.log("txFauxUnconfirmed: ", ptr, confirmations); + } + ); // callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), const directSendResult = ffi.Callback("void", [u64, bool], function (i, j) { console.log("directSendResult: ", i, j); diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 3ba81bbf0f..edb4a6957d 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -192,6 +192,10 @@ enum TransactionStatus { TRANSACTION_STATUS_NOT_FOUND = 7; // The transaction was rejected by the mempool TRANSACTION_STATUS_REJECTED = 8; + // This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found + TRANSACTION_STATUS_FAUX_UNCONFIRMED = 9; + // All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed + TRANSACTION_STATUS_FAUX_CONFIRMED = 10; } message GetCompletedTransactionsRequest { } diff --git a/applications/tari_app_grpc/src/conversions/transaction.rs b/applications/tari_app_grpc/src/conversions/transaction.rs index fffb851ef7..f74dc1e66f 100644 --- a/applications/tari_app_grpc/src/conversions/transaction.rs +++ b/applications/tari_app_grpc/src/conversions/transaction.rs @@ -98,6 +98,8 @@ impl From for grpc::TransactionStatus { Pending => grpc::TransactionStatus::Pending, Coinbase => grpc::TransactionStatus::Coinbase, Rejected => grpc::TransactionStatus::Rejected, + FauxUnconfirmed => grpc::TransactionStatus::FauxUnconfirmed, + FauxConfirmed => grpc::TransactionStatus::FauxConfirmed, } } } diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index 8e341008c6..d01aadd520 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -94,13 +94,15 @@ impl WalletEventMonitor { self.trigger_balance_refresh(); notifier.transaction_received(tx_id); }, - TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} => { + TransactionEvent::TransactionMinedUnconfirmed{tx_id, num_confirmations, is_valid: _} | + TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _}=> { self.trigger_confirmations_refresh(tx_id, num_confirmations).await; self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); notifier.transaction_mined_unconfirmed(tx_id, num_confirmations); }, - TransactionEvent::TransactionMined{tx_id, is_valid: _} => { + TransactionEvent::TransactionMined{tx_id, is_valid: _} | + TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _}=> { self.trigger_confirmations_cleanup(tx_id).await; self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); @@ -114,7 +116,8 @@ impl WalletEventMonitor { TransactionEvent::ReceivedTransaction(tx_id) | TransactionEvent::ReceivedTransactionReply(tx_id) | TransactionEvent::TransactionBroadcast(tx_id) | - TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | TransactionEvent::TransactionImported(tx_id) => { + TransactionEvent::TransactionMinedRequestTimedOut(tx_id) | + TransactionEvent::TransactionImported(tx_id) => { self.trigger_tx_state_refresh(tx_id).await; self.trigger_balance_refresh(); }, diff --git a/base_layer/common_types/src/transaction.rs b/base_layer/common_types/src/transaction.rs index 0e33ca237e..45908a7c89 100644 --- a/base_layer/common_types/src/transaction.rs +++ b/base_layer/common_types/src/transaction.rs @@ -17,7 +17,7 @@ pub enum TransactionStatus { Broadcast, /// This transaction has been mined and included in a block. MinedUnconfirmed, - /// This transaction was generated as part of importing a spendable UTXO + /// This transaction was generated as part of importing a spendable unblinded UTXO Imported, /// This transaction is still being negotiated by the parties Pending, @@ -27,6 +27,27 @@ pub enum TransactionStatus { MinedConfirmed, /// This transaction was Rejected by the mempool Rejected, + /// This is faux transaction mainly for one-sided transaction outputs or wallet recovery outputs have been found + FauxUnconfirmed, + /// All Imported and FauxUnconfirmed transactions will end up with this status when the outputs have been confirmed + FauxConfirmed, +} + +impl TransactionStatus { + pub fn is_faux(&self) -> bool { + match self { + TransactionStatus::Completed => false, + TransactionStatus::Broadcast => false, + TransactionStatus::MinedUnconfirmed => false, + TransactionStatus::Imported => true, + TransactionStatus::Pending => false, + TransactionStatus::Coinbase => false, + TransactionStatus::MinedConfirmed => false, + TransactionStatus::Rejected => false, + TransactionStatus::FauxUnconfirmed => true, + TransactionStatus::FauxConfirmed => true, + } + } } #[derive(Debug, Error)] @@ -48,6 +69,8 @@ impl TryFrom for TransactionStatus { 5 => Ok(TransactionStatus::Coinbase), 6 => Ok(TransactionStatus::MinedConfirmed), 7 => Ok(TransactionStatus::Rejected), + 8 => Ok(TransactionStatus::FauxUnconfirmed), + 9 => Ok(TransactionStatus::FauxConfirmed), code => Err(TransactionConversionError { code }), } } @@ -71,6 +94,43 @@ impl Display for TransactionStatus { TransactionStatus::Pending => write!(f, "Pending"), TransactionStatus::Coinbase => write!(f, "Coinbase"), TransactionStatus::Rejected => write!(f, "Rejected"), + TransactionStatus::FauxUnconfirmed => write!(f, "FauxUnconfirmed"), + TransactionStatus::FauxConfirmed => write!(f, "FauxConfirmed"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ImportStatus { + /// This transaction import status is used when importing a spendable UTXO + Imported, + /// This transaction import status is used when a one-sided transaction has been scanned but is unconfirmed + FauxUnconfirmed, + /// This transaction import status is used when a one-sided transaction has been scanned and confirmed + FauxConfirmed, +} + +impl TryFrom for TransactionStatus { + type Error = TransactionConversionError; + + fn try_from(value: ImportStatus) -> Result { + match value { + ImportStatus::Imported => Ok(TransactionStatus::Imported), + ImportStatus::FauxUnconfirmed => Ok(TransactionStatus::FauxUnconfirmed), + ImportStatus::FauxConfirmed => Ok(TransactionStatus::FauxConfirmed), + } + } +} + +impl TryFrom for ImportStatus { + type Error = TransactionConversionError; + + fn try_from(value: TransactionStatus) -> Result { + match value { + TransactionStatus::Imported => Ok(ImportStatus::Imported), + TransactionStatus::FauxUnconfirmed => Ok(ImportStatus::FauxUnconfirmed), + TransactionStatus::FauxConfirmed => Ok(ImportStatus::FauxConfirmed), + _ => Err(TransactionConversionError { code: i32::MAX }), } } } diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 315c931a6a..9bfd99a350 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -25,7 +25,7 @@ use std::{fmt, fmt::Formatter, sync::Arc}; use aes_gcm::Aes256Gcm; use tari_common_types::{ transaction::TxId, - types::{HashOutput, PublicKey}, + types::{BlockHash, HashOutput, PublicKey}, }; use tari_core::{ covenants::Covenant, @@ -106,8 +106,14 @@ pub enum OutputManagerRequest { num_kernels: usize, num_outputs: usize, }, - ScanForRecoverableOutputs(Vec), - ScanOutputs(Vec), + ScanForRecoverableOutputs { + outputs: Vec, + tx_id: TxId, + }, + ScanOutputs { + outputs: Vec, + tx_id: TxId, + }, AddKnownOneSidedPaymentScript(KnownOneSidedPaymentScript), CreateOutputWithFeatures { value: MicroTari, @@ -165,8 +171,8 @@ impl fmt::Display for OutputManagerRequest { "FeeEstimate(amount: {}, fee_per_gram: {}, num_kernels: {}, num_outputs: {})", amount, fee_per_gram, num_kernels, num_outputs ), - ScanForRecoverableOutputs(_) => write!(f, "ScanForRecoverableOutputs"), - ScanOutputs(_) => write!(f, "ScanOutputs"), + ScanForRecoverableOutputs { .. } => write!(f, "ScanForRecoverableOutputs"), + ScanOutputs { .. } => write!(f, "ScanOutputs"), AddKnownOneSidedPaymentScript(_) => write!(f, "AddKnownOneSidedPaymentScript"), CreateOutputWithFeatures { value, features } => { write!(f, "CreateOutputWithFeatures({}, {})", value, features,) @@ -220,12 +226,21 @@ pub enum OutputManagerResponse { RewoundOutputs(Vec), ScanOutputs(Vec), AddKnownOneSidedPaymentScript, - CreateOutputWithFeatures { output: Box }, - CreatePayToSelfWithOutputs { transaction: Box, tx_id: TxId }, + CreateOutputWithFeatures { + output: Box, + }, + CreatePayToSelfWithOutputs { + transaction: Box, + tx_id: TxId, + }, ReinstatedCancelledInboundTx, CoinbaseAbandonedSet, ClaimHtlcTransaction((TxId, MicroTari, MicroTari, Transaction)), - OutputStatusesByTxId(Vec), + OutputStatusesByTxId { + statuses: Vec, + mined_height: Option, + block_hash: Option, + }, } pub type OutputManagerEventSender = broadcast::Sender>; @@ -642,10 +657,11 @@ impl OutputManagerHandle { pub async fn scan_for_recoverable_outputs( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { match self .handle - .call(OutputManagerRequest::ScanForRecoverableOutputs(outputs)) + .call(OutputManagerRequest::ScanForRecoverableOutputs { outputs, tx_id }) .await?? { OutputManagerResponse::RewoundOutputs(outputs) => Ok(outputs), @@ -656,8 +672,13 @@ impl OutputManagerHandle { pub async fn scan_outputs_for_one_sided_payments( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { - match self.handle.call(OutputManagerRequest::ScanOutputs(outputs)).await?? { + match self + .handle + .call(OutputManagerRequest::ScanOutputs { outputs, tx_id }) + .await?? + { OutputManagerResponse::ScanOutputs(outputs) => Ok(outputs), _ => Err(OutputManagerError::UnexpectedApiResponse), } @@ -749,13 +770,20 @@ impl OutputManagerHandle { } } - pub async fn get_output_statuses_by_tx_id(&mut self, tx_id: TxId) -> Result, OutputManagerError> { + pub async fn get_output_statuses_by_tx_id( + &mut self, + tx_id: TxId, + ) -> Result<(Vec, Option, Option), OutputManagerError> { match self .handle .call(OutputManagerRequest::GetOutputStatusesByTxId(tx_id)) .await?? { - OutputManagerResponse::OutputStatusesByTxId(s) => Ok(s), + OutputManagerResponse::OutputStatusesByTxId { + statuses, + mined_height, + block_hash, + } => Ok((statuses, mined_height, block_hash)), _ => Err(OutputManagerError::UnexpectedApiResponse), } } diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index d6f4200e0c..afa80139f6 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -24,7 +24,10 @@ use std::{sync::Arc, time::Instant}; use log::*; use rand::rngs::OsRng; -use tari_common_types::types::{PrivateKey, PublicKey, RangeProof}; +use tari_common_types::{ + transaction::TxId, + types::{PrivateKey, PublicKey, RangeProof}, +}; use tari_core::transactions::{ transaction::{TransactionOutput, UnblindedOutput}, CryptoFactories, @@ -73,6 +76,7 @@ where TBackend: OutputManagerBackend + 'static pub async fn scan_and_recover_outputs( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { let start = Instant::now(); let outputs_length = outputs.len(); @@ -133,7 +137,7 @@ where TBackend: OutputManagerBackend + 'static Some(proof), )?; let output_hex = db_output.commitment.to_hex(); - if let Err(e) = self.db.add_unspent_output(db_output).await { + if let Err(e) = self.db.add_unspent_output_with_tx_id(tx_id, db_output).await { match e { OutputManagerStorageError::DuplicateOutput => { info!( diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 3dfb07ecc1..cef6150185 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -29,7 +29,7 @@ use log::*; use rand::{rngs::OsRng, RngCore}; use tari_common_types::{ transaction::TxId, - types::{HashOutput, PrivateKey, PublicKey}, + types::{BlockHash, HashOutput, PrivateKey, PublicKey}, }; use tari_comms::{types::CommsPublicKey, NodeIdentity}; use tari_core::{ @@ -358,16 +358,16 @@ where OutputManagerRequest::GetPublicRewindKeys => Ok(OutputManagerResponse::PublicRewindKeys(Box::new( self.resources.master_key_manager.get_rewind_public_keys(), ))), - OutputManagerRequest::ScanForRecoverableOutputs(outputs) => StandardUtxoRecoverer::new( + OutputManagerRequest::ScanForRecoverableOutputs { outputs, tx_id } => StandardUtxoRecoverer::new( self.resources.master_key_manager.clone(), self.resources.factories.clone(), self.resources.db.clone(), ) - .scan_and_recover_outputs(outputs) + .scan_and_recover_outputs(outputs, tx_id) .await .map(OutputManagerResponse::RewoundOutputs), - OutputManagerRequest::ScanOutputs(outputs) => self - .scan_outputs_for_one_sided_payments(outputs) + OutputManagerRequest::ScanOutputs { outputs, tx_id } => self + .scan_outputs_for_one_sided_payments(outputs, tx_id) .await .map(OutputManagerResponse::ScanOutputs), OutputManagerRequest::AddKnownOneSidedPaymentScript(known_script) => self @@ -415,16 +415,36 @@ where .create_htlc_refund_transaction(output, fee_per_gram) .await .map(OutputManagerResponse::ClaimHtlcTransaction), - OutputManagerRequest::GetOutputStatusesByTxId(tx_id) => self - .get_output_status_by_tx_id(tx_id) - .await - .map(OutputManagerResponse::OutputStatusesByTxId), + OutputManagerRequest::GetOutputStatusesByTxId(tx_id) => { + let (statuses, mined_height, block_hash) = self.get_output_status_by_tx_id(tx_id).await?; + Ok(OutputManagerResponse::OutputStatusesByTxId { + statuses, + mined_height, + block_hash, + }) + }, } } - async fn get_output_status_by_tx_id(&self, tx_id: TxId) -> Result, OutputManagerError> { + async fn get_output_status_by_tx_id( + &self, + tx_id: TxId, + ) -> Result<(Vec, Option, Option), OutputManagerError> { let outputs = self.resources.db.fetch_outputs_by_tx_id(tx_id).await?; - Ok(outputs.into_iter().map(|uo| uo.status).collect()) + let statuses = outputs.clone().into_iter().map(|uo| uo.status).collect(); + // We need the maximum mined height and corresponding block hash (faux transactions outputs can have different + // mined heights) + let (mut last_height, mut max_mined_height, mut block_hash) = (0u64, None, None); + for uo in outputs { + if let Some(height) = uo.mined_height { + if last_height < height { + last_height = height; + max_mined_height = uo.mined_height; + block_hash = uo.mined_in_block.clone(); + } + } + } + Ok((statuses, max_mined_height, block_hash)) } async fn claim_sha_atomic_swap_with_hash( @@ -1893,6 +1913,7 @@ where async fn scan_outputs_for_one_sided_payments( &mut self, outputs: Vec, + tx_id: TxId, ) -> Result, OutputManagerError> { let known_one_sided_payment_scripts: Vec = self.resources.db.get_all_known_one_sided_payment_scripts().await?; @@ -1945,7 +1966,7 @@ where )?; let output_hex = output.commitment.to_hex(); - match self.resources.db.add_unspent_output(db_output).await { + match self.resources.db.add_unspent_output_with_tx_id(tx_id, db_output).await { Ok(_) => { rewound_outputs.push(rewound_output); }, diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 8a9fde45dd..d996d7cf74 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -23,7 +23,10 @@ use std::{collections::HashMap, fmt, fmt::Formatter, sync::Arc}; use aes_gcm::Aes256Gcm; -use tari_common_types::{transaction::TxId, types::PublicKey}; +use tari_common_types::{ + transaction::{ImportStatus, TxId}, + types::PublicKey, +}; use tari_comms::types::CommsPublicKey; use tari_core::transactions::{ tari_amount::MicroTari, @@ -72,7 +75,15 @@ pub enum TransactionServiceRequest { }, SendShaAtomicSwapTransaction(CommsPublicKey, MicroTari, MicroTari, String), CancelTransaction(TxId), - ImportUtxo(MicroTari, CommsPublicKey, String, Option), + ImportUtxoWithStatus { + amount: MicroTari, + source_public_key: CommsPublicKey, + message: String, + maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option, + }, SubmitTransactionToSelf(TxId, Transaction, MicroTari, MicroTari, String), SetLowPowerMode, SetNormalPowerMode, @@ -123,12 +134,23 @@ impl fmt::Display for TransactionServiceRequest { f.write_str(&format!("SendShaAtomicSwapTransaction (to {}, {}, {})", k, v, msg)) }, Self::CancelTransaction(t) => f.write_str(&format!("CancelTransaction ({})", t)), - Self::ImportUtxo(v, k, msg, maturity) => f.write_str(&format!( - "ImportUtxo (from {}, {}, {} with maturity: {})", - k, - v, - msg, - maturity.unwrap_or(0) + Self::ImportUtxoWithStatus { + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + } => f.write_str(&format!( + "ImportUtxo (from {}, {}, {} with maturity {} and {:?} and {:?} and {:?})", + source_public_key, + amount, + message, + maturity.unwrap_or(0), + import_status, + tx_id, + current_height, )), Self::SubmitTransactionToSelf(tx_id, _, _, _, _) => f.write_str(&format!("SubmitTransaction ({})", tx_id)), Self::SetLowPowerMode => f.write_str("SetLowPowerMode "), @@ -189,6 +211,15 @@ pub enum TransactionEvent { TransactionCancelled(TxId, TxRejection), TransactionBroadcast(TxId), TransactionImported(TxId), + FauxTransactionUnconfirmed { + tx_id: TxId, + num_confirmations: u64, + is_valid: bool, + }, + FauxTransactionConfirmed { + tx_id: TxId, + is_valid: bool, + }, TransactionMined { tx_id: TxId, is_valid: bool, @@ -242,6 +273,20 @@ impl fmt::Display for TransactionEvent { TransactionEvent::TransactionImported(tx) => { write!(f, "TransactionImported for {}", tx) }, + TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations, + is_valid, + } => { + write!( + f, + "FauxTransactionUnconfirmed for {} with num confirmations: {}. is_valid: {}", + tx_id, num_confirmations, is_valid + ) + }, + TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid } => { + write!(f, "FauxTransactionConfirmed for {}. is_valid: {}", tx_id, is_valid) + }, TransactionEvent::TransactionMined { tx_id, is_valid } => { write!(f, "TransactionMined for {}. is_valid: {}", tx_id, is_valid) }, @@ -517,21 +562,27 @@ impl TransactionServiceHandle { } } - pub async fn import_utxo( + pub async fn import_utxo_with_status( &mut self, amount: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option, ) -> Result { match self .handle - .call(TransactionServiceRequest::ImportUtxo( + .call(TransactionServiceRequest::ImportUtxoWithStatus { amount, source_public_key, message, maturity, - )) + import_status, + tx_id, + current_height, + }) .await?? { TransactionServiceResponse::UtxoImported(tx_id) => Ok(tx_id), diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index d32efc4900..3d5f8189b5 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -451,6 +451,7 @@ where inbound_tx.timestamp, TransactionDirection::Inbound, None, + None, ); self.resources diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 4007eeaa77..c9e92680ac 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -511,6 +511,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ); self.resources diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index b348c29758..3572317408 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -396,12 +396,23 @@ where mined_in_block.clone(), num_confirmations, num_confirmations >= self.config.num_confirmations_required, + status.is_faux(), ) .await .for_protocol(self.operation_id.as_u64())?; if num_confirmations >= self.config.num_confirmations_required { - self.publish_event(TransactionEvent::TransactionMined { tx_id, is_valid: true }) + if status.is_faux() { + self.publish_event(TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }) + } else { + self.publish_event(TransactionEvent::TransactionMined { tx_id, is_valid: true }) + } + } else if status.is_faux() { + self.publish_event(TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations, + is_valid: true, + }) } else { self.publish_event(TransactionEvent::TransactionMinedUnconfirmed { tx_id, @@ -441,6 +452,7 @@ where mined_in_block.clone(), num_confirmations, num_confirmations >= self.config.num_confirmations_required, + false, ) .await .for_protocol(self.operation_id.as_u64())?; diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 7ce9c6a77b..4389a4c955 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -34,7 +34,7 @@ use log::*; use rand::rngs::OsRng; use sha2::Sha256; use tari_common_types::{ - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{PrivateKey, PublicKey}, }; use tari_comms::{peer_manager::NodeIdentity, types::CommsPublicKey}; @@ -93,7 +93,7 @@ use crate::{ models::CompletedTransaction, }, tasks::{ - check_imported_transaction_status::check_imported_transactions, + check_faux_transaction_status::check_faux_transactions, send_finalized_transaction::send_finalized_transaction_message, send_transaction_cancelled::send_transaction_cancelled_message, send_transaction_reply::send_transaction_reply, @@ -644,8 +644,24 @@ where TransactionServiceRequest::GetAnyTransaction(tx_id) => Ok(TransactionServiceResponse::AnyTransaction( Box::new(self.db.get_any_transaction(tx_id).await?), )), - TransactionServiceRequest::ImportUtxo(value, source_public_key, message, maturity) => self - .add_utxo_import_transaction(value, source_public_key, message, maturity) + TransactionServiceRequest::ImportUtxoWithStatus { + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + } => self + .add_utxo_import_transaction_with_status( + amount, + source_public_key, + message, + maturity, + import_status, + tx_id, + current_height, + ) .await .map(TransactionServiceResponse::UtxoImported), TransactionServiceRequest::SubmitTransactionToSelf(tx_id, tx, fee, amount, message) => self @@ -748,7 +764,21 @@ where if let OutputManagerEvent::TxoValidationSuccess(_) = (*event).clone() { let db = self.db.clone(); let output_manager_handle = self.output_manager_service.clone(); - tokio::spawn(check_imported_transactions(output_manager_handle, db)); + let metadata = match self.wallet_db.get_chain_metadata().await { + Ok(data) => data, + Err(_) => None, + }; + let tip_height = match metadata { + Some(val) => val.height_of_longest_chain(), + None => 0u64, + }; + let event_publisher = self.event_publisher.clone(); + tokio::spawn(check_faux_transactions( + output_manager_handle, + db, + event_publisher, + tip_height, + )); } } @@ -812,6 +842,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ), ) .await?; @@ -1016,6 +1047,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ), ) .await?; @@ -1159,6 +1191,7 @@ where Utc::now().naive_utc(), TransactionDirection::Outbound, None, + None, ), ) .await?; @@ -2023,35 +2056,46 @@ where } /// Add a completed transaction to the Transaction Manager to record directly importing a spendable UTXO. - pub async fn add_utxo_import_transaction( + pub async fn add_utxo_import_transaction_with_status( &mut self, value: MicroTari, source_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + tx_id: Option, + current_height: Option, ) -> Result { - let tx_id = TxId::new_random(); + let tx_id = if let Some(id) = tx_id { id } else { TxId::new_random() }; self.db - .add_utxo_import_transaction( + .add_utxo_import_transaction_with_status( tx_id, value, source_public_key, self.node_identity.public_key().clone(), message, maturity, + import_status.clone(), + current_height, ) .await?; - let _ = self - .event_publisher - .send(Arc::new(TransactionEvent::TransactionImported(tx_id))) - .map_err(|e| { - trace!( - target: LOG_TARGET, - "Error sending event, usually because there are no subscribers: {:?}", - e - ); + let transaction_event = match import_status { + ImportStatus::Imported => TransactionEvent::TransactionImported(tx_id), + ImportStatus::FauxUnconfirmed => TransactionEvent::FauxTransactionUnconfirmed { + tx_id, + num_confirmations: 0, + is_valid: true, + }, + ImportStatus::FauxConfirmed => TransactionEvent::FauxTransactionConfirmed { tx_id, is_valid: true }, + }; + let _ = self.event_publisher.send(Arc::new(transaction_event)).map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event, usually because there are no subscribers: {:?}", e - }); + ); + e + }); Ok(tx_id) } @@ -2105,6 +2149,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ), ) .await?; @@ -2165,6 +2210,7 @@ where Utc::now().naive_utc(), TransactionDirection::Inbound, Some(block_height), + None, ), ) .await?; diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index aede6aad88..d55a794272 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -22,6 +22,7 @@ use std::{ collections::HashMap, + convert::TryFrom, fmt, fmt::{Display, Error, Formatter}, sync::Arc, @@ -31,7 +32,7 @@ use aes_gcm::Aes256Gcm; use chrono::Utc; use log::*; use tari_common_types::{ - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{BlindingFactor, BlockHash}, }; use tari_comms::types::CommsPublicKey; @@ -132,6 +133,7 @@ pub trait TransactionBackend: Send + Sync + Clone { mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError>; /// Clears the mined block and height of a transaction fn set_transaction_as_unmined(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; @@ -142,6 +144,11 @@ pub trait TransactionBackend: Send + Sync + Clone { &self, ) -> Result, TransactionStorageError>; fn fetch_imported_transactions(&self) -> Result, TransactionStorageError>; + fn fetch_unconfirmed_faux_transactions(&self) -> Result, TransactionStorageError>; + fn fetch_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError>; } #[derive(Clone, PartialEq)] @@ -442,6 +449,27 @@ where T: TransactionBackend + 'static Ok(t) } + pub async fn get_unconfirmed_faux_transactions( + &self, + ) -> Result, TransactionStorageError> { + let db_clone = self.db.clone(); + let t = tokio::task::spawn_blocking(move || db_clone.fetch_unconfirmed_faux_transactions()) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(t) + } + + pub async fn get_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError> { + let db_clone = self.db.clone(); + let t = tokio::task::spawn_blocking(move || db_clone.fetch_confirmed_faux_transactions_from_height(height)) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(t) + } + pub async fn fetch_last_mined_transaction(&self) -> Result, TransactionStorageError> { self.db.fetch_last_mined_transaction() } @@ -702,7 +730,8 @@ where T: TransactionBackend + 'static .and_then(|inner_result| inner_result) } - pub async fn add_utxo_import_transaction( + /// Faux transaction added to the database with imported status + pub async fn add_utxo_import_transaction_with_status( &self, tx_id: TxId, amount: MicroTari, @@ -710,6 +739,8 @@ where T: TransactionBackend + 'static comms_public_key: CommsPublicKey, message: String, maturity: Option, + import_status: ImportStatus, + current_height: Option, ) -> Result<(), TransactionStorageError> { let transaction = CompletedTransaction::new( tx_id, @@ -724,11 +755,12 @@ where T: TransactionBackend + 'static BlindingFactor::default(), BlindingFactor::default(), ), - TransactionStatus::Imported, + TransactionStatus::try_from(import_status)?, message, Utc::now().naive_utc(), TransactionDirection::Inbound, maturity, + current_height, ); let db_clone = self.db.clone(); @@ -816,6 +848,7 @@ where T: TransactionBackend + 'static mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let db_clone = self.db.clone(); tokio::task::spawn_blocking(move || { @@ -826,6 +859,7 @@ where T: TransactionBackend + 'static mined_in_block, num_confirmations, is_confirmed, + is_faux, ) }) .await diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs index d0c0702571..756ed3312b 100644 --- a/base_layer/wallet/src/transaction_service/storage/models.rs +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -159,6 +159,7 @@ impl CompletedTransaction { timestamp: NaiveDateTime, direction: TransactionDirection, coinbase_block_height: Option, + mined_height: Option, ) -> Self { let transaction_signature = if let Some(excess_sig) = transaction.first_kernel_excess_sig() { excess_sig.clone() @@ -183,7 +184,7 @@ impl CompletedTransaction { valid: true, transaction_signature, confirmations: None, - mined_height: None, + mined_height, mined_in_block: None, } } diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 8dc318d5e8..a3a6566a86 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -1009,6 +1009,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { mined_in_block: BlockHash, num_confirmations: u64, is_confirmed: bool, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; @@ -1022,6 +1023,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { num_confirmations, is_confirmed, &conn, + is_faux, )?; }, Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { @@ -1048,6 +1050,8 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); let tx = completed_transactions::table + // Note: Check 'mined_in_block' as well as 'mined_height' is populated for faux transactions before it is confirmed + .filter(completed_transactions::mined_in_block.is_not_null()) .filter(completed_transactions::mined_height.is_not_null()) .filter(completed_transactions::mined_height.gt(0)) .order_by(completed_transactions::mined_height.desc()) @@ -1072,6 +1076,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(result) } + // This method returns completed but unconfirmed transactions that were not imported fn fetch_unconfirmed_transactions_info(&self) -> Result, TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; @@ -1226,6 +1231,40 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { }) .collect::, TransactionStorageError>>() } + + fn fetch_unconfirmed_faux_transactions(&self) -> Result, TransactionStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + CompletedTransactionSql::index_by_status_and_cancelled(TransactionStatus::FauxUnconfirmed, false, &conn)? + .into_iter() + .map(|mut ct: CompletedTransactionSql| { + if let Err(e) = self.decrypt_if_necessary(&mut ct) { + return Err(e); + } + CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) + }) + .collect::, TransactionStorageError>>() + } + + fn fetch_confirmed_faux_transactions_from_height( + &self, + height: u64, + ) -> Result, TransactionStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + CompletedTransactionSql::index_by_status_and_cancelled_from_block_height( + TransactionStatus::FauxConfirmed, + false, + height as i64, + &conn, + )? + .into_iter() + .map(|mut ct: CompletedTransactionSql| { + if let Err(e) = self.decrypt_if_necessary(&mut ct) { + return Err(e); + } + CompletedTransaction::try_from(ct).map_err(TransactionStorageError::from) + }) + .collect::, TransactionStorageError>>() + } } #[derive(Debug, PartialEq)] @@ -1674,6 +1713,19 @@ impl CompletedTransactionSql { .load::(conn)?) } + pub fn index_by_status_and_cancelled_from_block_height( + status: TransactionStatus, + cancelled: bool, + block_height: i64, + conn: &SqliteConnection, + ) -> Result, TransactionStorageError> { + Ok(completed_transactions::table + .filter(completed_transactions::cancelled.eq(cancelled as i32)) + .filter(completed_transactions::status.eq(status as i32)) + .filter(completed_transactions::mined_height.ge(block_height)) + .load::(conn)?) + } + pub fn index_coinbase_at_block_height( block_height: i64, conn: &SqliteConnection, @@ -1753,6 +1805,8 @@ impl CompletedTransactionSql { pub fn set_as_unmined(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { let status = if self.coinbase_block_height.is_some() { Some(TransactionStatus::Coinbase as i32) + } else if self.status == TransactionStatus::FauxConfirmed as i32 { + Some(TransactionStatus::FauxUnconfirmed as i32) } else if self.status == TransactionStatus::Broadcast as i32 { Some(TransactionStatus::Broadcast as i32) } else { @@ -1798,11 +1852,18 @@ impl CompletedTransactionSql { num_confirmations: u64, is_confirmed: bool, conn: &SqliteConnection, + is_faux: bool, ) -> Result<(), TransactionStorageError> { let status = if self.coinbase_block_height.is_some() && !is_valid { TransactionStatus::Coinbase as i32 } else if is_confirmed { - TransactionStatus::MinedConfirmed as i32 + if is_faux { + TransactionStatus::FauxConfirmed as i32 + } else { + TransactionStatus::MinedConfirmed as i32 + } + } else if is_faux { + TransactionStatus::FauxUnconfirmed as i32 } else { TransactionStatus::MinedUnconfirmed as i32 }; @@ -1983,7 +2044,7 @@ pub struct UnconfirmedTransactionInfoSql { } impl UnconfirmedTransactionInfoSql { - /// This method returns completed but unconfirmed transactions + /// This method returns completed but unconfirmed transactions that were not imported or scanned pub fn fetch_unconfirmed_transactions_info( conn: &SqliteConnection, ) -> Result, TransactionStorageError> { @@ -1999,6 +2060,8 @@ impl UnconfirmedTransactionInfoSql { .filter( completed_transactions::status .ne(TransactionStatus::Imported as i32) + .and(completed_transactions::status.ne(TransactionStatus::FauxUnconfirmed as i32)) + .and(completed_transactions::status.ne(TransactionStatus::FauxConfirmed as i32)) .and( completed_transactions::mined_height .is_null() diff --git a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs new file mode 100644 index 0000000000..6a5a776fe4 --- /dev/null +++ b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs @@ -0,0 +1,167 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::sync::Arc; + +use log::*; +use tari_common_types::types::BlockHash; + +use crate::{ + output_manager_service::{handle::OutputManagerHandle, storage::OutputStatus}, + transaction_service::{ + config::TransactionServiceConfig, + handle::{TransactionEvent, TransactionEventSender}, + storage::{ + database::{TransactionBackend, TransactionDatabase}, + models::CompletedTransaction, + }, + }, +}; + +const LOG_TARGET: &str = "wallet::transaction_service::service"; + +pub async fn check_faux_transactions( + mut output_manager: OutputManagerHandle, + db: TransactionDatabase, + event_publisher: TransactionEventSender, + tip_height: u64, +) { + let mut all_faux_transactions: Vec = match db.get_imported_transactions().await { + Ok(txs) => txs, + Err(e) => { + error!(target: LOG_TARGET, "Problem retrieving imported transactions: {}", e); + return; + }, + }; + let mut unconfirmed_faux = match db.get_unconfirmed_faux_transactions().await { + Ok(txs) => txs, + Err(e) => { + error!( + target: LOG_TARGET, + "Problem retrieving unconfirmed faux transactions: {}", e + ); + return; + }, + }; + all_faux_transactions.append(&mut unconfirmed_faux); + // Reorged faux transactions cannot be detected by excess signature, thus use last known confirmed transaction + // height or current tip height with safety margin to determine if these should be returned + let last_mined_transaction = match db.fetch_last_mined_transaction().await { + Ok(tx) => tx, + Err(_) => None, + }; + let height_with_margin = tip_height.saturating_sub(100); + let check_height = if let Some(tx) = last_mined_transaction { + tx.mined_height.unwrap_or(height_with_margin) + } else { + height_with_margin + }; + let mut confirmed_faux = match db.get_confirmed_faux_transactions_from_height(check_height).await { + Ok(txs) => txs, + Err(e) => { + error!( + target: LOG_TARGET, + "Problem retrieving confirmed faux transactions: {}", e + ); + return; + }, + }; + all_faux_transactions.append(&mut confirmed_faux); + + debug!( + target: LOG_TARGET, + "Checking {} faux transaction statuses", + all_faux_transactions.len() + ); + for tx in all_faux_transactions.into_iter() { + let (status, mined_height, block_hash) = match output_manager.get_output_statuses_by_tx_id(tx.tx_id).await { + Ok(s) => s, + Err(e) => { + error!(target: LOG_TARGET, "Problem retrieving output statuses: {}", e); + return; + }, + }; + if !status.iter().any(|s| s != &OutputStatus::Unspent) { + let mined_height = if let Some(height) = mined_height { + height + } else { + tip_height + }; + let mined_in_block: BlockHash = if let Some(hash) = block_hash { + hash + } else { + vec![0u8; 32] + }; + let is_valid = tip_height >= mined_height; + let is_confirmed = tip_height.saturating_sub(mined_height) >= + TransactionServiceConfig::default().num_confirmations_required; + let num_confirmations = tip_height - mined_height; + debug!( + target: LOG_TARGET, + "Updating faux transaction: TxId({}), mined_height({}), is_confirmed({}), num_confirmations({}), \ + is_valid({})", + tx.tx_id, + mined_height, + is_confirmed, + num_confirmations, + is_valid, + ); + let result = db + .set_transaction_mined_height( + tx.tx_id, + true, + mined_height, + mined_in_block, + num_confirmations, + is_confirmed, + is_valid, + ) + .await; + if let Err(e) = result { + error!( + target: LOG_TARGET, + "Error setting faux transaction to mined confirmed: {}", e + ); + } else { + let transaction_event = match is_confirmed { + false => TransactionEvent::FauxTransactionUnconfirmed { + tx_id: tx.tx_id, + num_confirmations: 0, + is_valid, + }, + true => TransactionEvent::FauxTransactionConfirmed { + tx_id: tx.tx_id, + is_valid, + }, + }; + let _ = event_publisher.send(Arc::new(transaction_event)).map_err(|e| { + trace!( + target: LOG_TARGET, + "Error sending event, usually because there are no subscribers: {:?}", + e + ); + e + }); + } + } + } +} diff --git a/base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs deleted file mode 100644 index fe1ea992b2..0000000000 --- a/base_layer/wallet/src/transaction_service/tasks/check_imported_transaction_status.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2021. The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use log::*; - -use crate::{ - output_manager_service::{handle::OutputManagerHandle, storage::OutputStatus}, - transaction_service::{ - config::TransactionServiceConfig, - storage::database::{TransactionBackend, TransactionDatabase}, - }, -}; - -const LOG_TARGET: &str = "wallet::transaction_service::service"; - -pub async fn check_imported_transactions( - mut output_manager: OutputManagerHandle, - db: TransactionDatabase, -) { - let imported_transactions = match db.get_imported_transactions().await { - Ok(txs) => txs, - Err(e) => { - error!(target: LOG_TARGET, "Problem retrieving imported transactions: {}", e); - return; - }, - }; - - for tx in imported_transactions.into_iter() { - let status = match output_manager.get_output_statuses_by_tx_id(tx.tx_id).await { - Ok(s) => s, - Err(e) => { - error!(target: LOG_TARGET, "Problem retrieving output statuses: {}", e); - return; - }, - }; - if !status.iter().any(|s| s != &OutputStatus::Unspent) { - debug!( - target: LOG_TARGET, - "Faux Transaction (TxId: {}) updated to confirmed", tx.tx_id - ); - if let Err(e) = db - .set_transaction_mined_height( - tx.tx_id, - true, - 0, - vec![0u8; 32], - TransactionServiceConfig::default().num_confirmations_required, - true, - ) - .await - { - error!( - target: LOG_TARGET, - "Error setting faux transaction to mined confirmed: {}", e - ); - } - } - } -} diff --git a/base_layer/wallet/src/transaction_service/tasks/mod.rs b/base_layer/wallet/src/transaction_service/tasks/mod.rs index dce38a144c..292932156e 100644 --- a/base_layer/wallet/src/transaction_service/tasks/mod.rs +++ b/base_layer/wallet/src/transaction_service/tasks/mod.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -pub mod check_imported_transaction_status; +pub mod check_faux_transaction_status; pub mod send_finalized_transaction; pub mod send_transaction_cancelled; pub mod send_transaction_reply; diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index a56bb36e4d..5eec755347 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -28,7 +28,10 @@ use std::{ use chrono::Utc; use futures::StreamExt; use log::*; -use tari_common_types::{transaction::TxId, types::HashOutput}; +use tari_common_types::{ + transaction::{ImportStatus, TxId}, + types::HashOutput, +}; use tari_comms::{peer_manager::NodeId, types::CommsPublicKey, PeerConnection}; use tari_core::{ base_node::rpc::BaseNodeWalletRpcClient, @@ -46,6 +49,7 @@ use tokio::sync::broadcast; use crate::{ error::WalletError, storage::database::WalletBackend, + transaction_service::error::{TransactionServiceError, TransactionStorageError}, utxo_scanner_service::{ error::UtxoScannerError, handle::UtxoScannerEvent, @@ -427,10 +431,12 @@ where TBackend: WalletBackend + 'static total_scanned += outputs.len(); let start = Instant::now(); - let found_outputs = self.scan_for_outputs(outputs).await?; + let (tx_id, found_outputs) = self.scan_for_outputs(outputs).await?; scan_for_outputs_profiling.push(start.elapsed()); - let (count, amount) = self.import_utxos_to_transaction_service(found_outputs).await?; + let (count, amount) = self + .import_utxos_to_transaction_service(found_outputs, tx_id, current_height) + .await?; self.resources .db @@ -481,17 +487,18 @@ where TBackend: WalletBackend + 'static async fn scan_for_outputs( &mut self, outputs: Vec, - ) -> Result, UtxoScannerError> { + ) -> Result<(TxId, Vec<(UnblindedOutput, String)>), UtxoScannerError> { let mut found_outputs: Vec<(UnblindedOutput, String)> = Vec::new(); + let tx_id = TxId::new_random(); if self.mode == UtxoScannerMode::Recovery { found_outputs.append( &mut self .resources .output_manager_service - .scan_for_recoverable_outputs(outputs.clone()) + .scan_for_recoverable_outputs(outputs.clone(), tx_id) .await? .into_iter() - .map(|v| (v, format!("Recovered on {}.", Utc::now().naive_utc()))) + .map(|uo| (uo, format!("Recovered output on {}.", Utc::now().naive_utc()))) .collect(), ); }; @@ -499,36 +506,54 @@ where TBackend: WalletBackend + 'static &mut self .resources .output_manager_service - .scan_outputs_for_one_sided_payments(outputs.clone()) + .scan_outputs_for_one_sided_payments(outputs.clone(), tx_id) .await? .into_iter() - .map(|v| { + .map(|uo| { ( - v, - format!("Detected one-sided transaction on {}.", Utc::now().naive_utc()), + uo, + format!("Detected one-sided transaction output on {}.", Utc::now().naive_utc()), ) }) .collect(), ); - Ok(found_outputs) + Ok((tx_id, found_outputs)) } async fn import_utxos_to_transaction_service( &mut self, utxos: Vec<(UnblindedOutput, String)>, + tx_id: TxId, + current_height: u64, ) -> Result<(u64, MicroTari), UtxoScannerError> { let mut num_recovered = 0u64; let mut total_amount = MicroTari::from(0); let source_public_key = self.resources.node_identity.public_key().clone(); - for uo in utxos { + for (uo, message) in utxos { match self - .import_unblinded_utxo_to_transaction_service(uo.0.clone(), &source_public_key, uo.1) + .import_unblinded_utxo_to_transaction_service( + uo.clone(), + &source_public_key, + message, + tx_id, + current_height, + ) .await { Ok(_) => { num_recovered = num_recovered.saturating_add(1); - total_amount += uo.0.value; + total_amount += uo.value; + }, + Err(WalletError::TransactionServiceError(TransactionServiceError::TransactionStorageError( + TransactionStorageError::DuplicateOutput, + ))) => { + info!( + target: LOG_TARGET, + "Recoverer attempted to add a duplicate output to the database for faux transaction ({}); \ + ignoring it as this is not a real error", + tx_id + ); }, Err(e) => return Err(UtxoScannerError::UtxoImportError(e.to_string())), } @@ -565,33 +590,38 @@ where TBackend: WalletBackend + 'static let _ = self.event_sender.send(event); } - /// A faux incoming transaction will be created to provide a record of the event of importing a UTXO. The TxId of - /// the generated transaction is returned. + /// A faux incoming transaction will be created to provide a record of the event of importing a scanned UTXO. The + /// TxId of the generated transaction is returned. pub async fn import_unblinded_utxo_to_transaction_service( &mut self, unblinded_output: UnblindedOutput, source_public_key: &CommsPublicKey, message: String, + tx_id: TxId, + current_height: u64, ) -> Result { let tx_id = self .resources .transaction_service - .import_utxo( + .import_utxo_with_status( unblinded_output.value, source_public_key.clone(), message, Some(unblinded_output.features.maturity), + ImportStatus::FauxUnconfirmed, + Some(tx_id), + Some(current_height), ) .await?; info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::FauxUnconfirmed'", unblinded_output .as_transaction_input(&self.resources.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? - .to_hex() + .to_hex(), ); Ok(tx_id) diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 46801d53ab..47ec10a00e 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -26,7 +26,7 @@ use digest::Digest; use log::*; use tari_common::configuration::bootstrap::ApplicationType; use tari_common_types::{ - transaction::TxId, + transaction::{ImportStatus, TxId}, types::{ComSignature, PrivateKey, PublicKey}, }; use tari_comms::{ @@ -401,7 +401,15 @@ where let tx_id = self .transaction_service - .import_utxo(amount, source_public_key.clone(), message, Some(features.maturity)) + .import_utxo_with_status( + amount, + source_public_key.clone(), + message, + Some(features.maturity), + ImportStatus::Imported, + None, + None, + ) .await?; let commitment_hex = unblinded_output @@ -416,7 +424,7 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", commitment_hex + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::Imported'", commitment_hex ); Ok(tx_id) @@ -433,11 +441,14 @@ where ) -> Result { let tx_id = self .transaction_service - .import_utxo( + .import_utxo_with_status( unblinded_output.value, source_public_key.clone(), message, Some(unblinded_output.features.maturity), + ImportStatus::Imported, + None, + None, ) .await?; @@ -447,12 +458,12 @@ where info!( target: LOG_TARGET, - "UTXO (Commitment: {}) imported into wallet", + "UTXO (Commitment: {}) imported into wallet as 'ImportStatus::Imported'", unblinded_output .as_transaction_input(&self.factories.commitment)? .commitment() .map_err(WalletError::TransactionError)? - .to_hex() + .to_hex(), ); Ok(tx_id) diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index dbe7909342..10de433c8d 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -1904,12 +1904,12 @@ async fn test_get_status_by_tx_id() { let (mut oms, _, _shutdown, _, _, _, _, _) = setup_output_manager_service(backend, true).await; let (_ti, uo1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - oms.add_unvalidated_output(TxId::from(1), uo1, None).await.unwrap(); + oms.add_unvalidated_output(TxId::from(1u64), uo1, None).await.unwrap(); let (_ti, uo2) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - oms.add_unvalidated_output(TxId::from(2), uo2, None).await.unwrap(); + oms.add_unvalidated_output(TxId::from(2u64), uo2, None).await.unwrap(); - let status = oms.get_output_statuses_by_tx_id(TxId::from(1)).await.unwrap(); + let (status, _, _) = oms.get_output_statuses_by_tx_id(TxId::from(1u64)).await.unwrap(); assert_eq!(status.len(), 1); assert_eq!(status[0], OutputStatus::EncumberedToBeReceived); diff --git a/base_layer/wallet/tests/support/output_manager_service_mock.rs b/base_layer/wallet/tests/support/output_manager_service_mock.rs index a409dd2c4c..1837418439 100644 --- a/base_layer/wallet/tests/support/output_manager_service_mock.rs +++ b/base_layer/wallet/tests/support/output_manager_service_mock.rs @@ -96,7 +96,10 @@ impl OutputManagerServiceMock { ) { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - OutputManagerRequest::ScanForRecoverableOutputs(requested_outputs) => { + OutputManagerRequest::ScanForRecoverableOutputs { + outputs: requested_outputs, + tx_id: _tx_id, + } => { let lock = acquire_lock!(self.state.recoverable_outputs); let outputs = (*lock) .clone() @@ -117,7 +120,10 @@ impl OutputManagerServiceMock { e }); }, - OutputManagerRequest::ScanOutputs(_to) => { + OutputManagerRequest::ScanOutputs { + outputs: _to, + tx_id: _tx_id, + } => { let lock = acquire_lock!(self.state.one_sided_payments); let outputs = (*lock).clone(); let _ = reply_tx diff --git a/base_layer/wallet/tests/support/transaction_service_mock.rs b/base_layer/wallet/tests/support/transaction_service_mock.rs index cce11b5707..f4bb390242 100644 --- a/base_layer/wallet/tests/support/transaction_service_mock.rs +++ b/base_layer/wallet/tests/support/transaction_service_mock.rs @@ -95,9 +95,9 @@ impl TransactionServiceMock { info!(target: LOG_TARGET, "Handling Request: {}", request); match request { - TransactionServiceRequest::ImportUtxo(_, _, _, _) => { + TransactionServiceRequest::ImportUtxoWithStatus { .. } => { let _ = reply_tx - .send(Ok(TransactionServiceResponse::UtxoImported(TxId::from(42)))) + .send(Ok(TransactionServiceResponse::UtxoImported(TxId::from(42u64)))) .map_err(|e| { warn!(target: LOG_TARGET, "Failed to send reply"); e diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index b145f8ef8c..b2632638d7 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -38,7 +38,7 @@ use prost::Message; use rand::rngs::OsRng; use tari_common_types::{ chain_metadata::ChainMetadata, - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, types::{PrivateKey, PublicKey, Signature}, }; use tari_comms::{ @@ -908,7 +908,7 @@ fn recover_one_sided_transaction() { let outputs = completed_tx.transaction.body.outputs().clone(); let unblinded = bob_oms - .scan_outputs_for_one_sided_payments(outputs.clone()) + .scan_outputs_for_one_sided_payments(outputs.clone(), TxId::new_random()) .await .unwrap(); // Bob should be able to claim 1 output. @@ -916,7 +916,10 @@ fn recover_one_sided_transaction() { assert_eq!(value, unblinded[0].value); // Should ignore already existing outputs - let unblinded = bob_oms.scan_outputs_for_one_sided_payments(outputs).await.unwrap(); + let unblinded = bob_oms + .scan_outputs_for_one_sided_payments(outputs, TxId::new_random()) + .await + .unwrap(); assert!(unblinded.is_empty()); }); } @@ -5394,52 +5397,115 @@ fn test_update_faux_tx_on_oms_validation() { let mut alice_ts_interface = setup_transaction_service_no_comms(&mut runtime, factories.clone(), connection, None); - let tx_id = runtime - .block_on(alice_ts_interface.transaction_service_handle.import_utxo( + let tx_id_1 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( MicroTari::from(10000), alice_ts_interface.base_node_identity.public_key().clone(), "blah".to_string(), None, + ImportStatus::Imported, + None, + None, )) .unwrap(); - - let (_ti, uo) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); - runtime - .block_on( - alice_ts_interface - .output_manager_service_handle - .add_output_with_tx_id(tx_id, uo, None), - ) + let tx_id_2 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( + MicroTari::from(20000), + alice_ts_interface.base_node_identity.public_key().clone(), + "one-sided 1".to_string(), + None, + ImportStatus::FauxUnconfirmed, + None, + None, + )) .unwrap(); - - let transaction = runtime - .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) - .unwrap() + let tx_id_3 = runtime + .block_on(alice_ts_interface.transaction_service_handle.import_utxo_with_status( + MicroTari::from(30000), + alice_ts_interface.base_node_identity.public_key().clone(), + "one-sided 2".to_string(), + None, + ImportStatus::FauxConfirmed, + None, + None, + )) .unwrap(); - if let WalletTransaction::Completed(tx) = transaction { - assert_eq!(tx.status, TransactionStatus::Imported); - } else { - panic!("Should find a complete transaction"); + + let (_ti, uo_1) = make_input(&mut OsRng.clone(), MicroTari::from(10000), &factories.commitment); + let (_ti, uo_2) = make_input(&mut OsRng.clone(), MicroTari::from(20000), &factories.commitment); + let (_ti, uo_3) = make_input(&mut OsRng.clone(), MicroTari::from(30000), &factories.commitment); + for (tx_id, uo) in [(tx_id_1, uo_1), (tx_id_2, uo_2), (tx_id_3, uo_3)] { + runtime + .block_on( + alice_ts_interface + .output_manager_service_handle + .add_output_with_tx_id(tx_id, uo, None), + ) + .unwrap(); } + for tx_id in [tx_id_1, tx_id_2, tx_id_3] { + let transaction = runtime + .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) + .unwrap() + .unwrap(); + if tx_id == tx_id_1 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::Imported); + } else { + panic!("Should find a complete Imported transaction"); + } + } + if tx_id == tx_id_2 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::FauxUnconfirmed); + } else { + panic!("Should find a complete FauxUnconfirmed transaction"); + } + } + if tx_id == tx_id_3 { + if let WalletTransaction::Completed(tx) = &transaction { + assert_eq!(tx.status, TransactionStatus::FauxConfirmed); + } else { + panic!("Should find a complete FauxConfirmed transaction"); + } + } + } + + // This will change the status of the imported transaction alice_ts_interface .output_manager_service_event_publisher - .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1))) + .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1u64))) .unwrap(); - let mut found = false; + let mut found_imported = false; + let mut found_faux_unconfirmed = false; + let mut found_faux_confirmed = false; for _ in 0..20 { runtime.block_on(async { sleep(Duration::from_secs(1)).await }); - let transaction = runtime - .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) - .unwrap() - .unwrap(); - if let WalletTransaction::Completed(tx) = transaction { - if tx.status == TransactionStatus::MinedConfirmed { - found = true; - break; + for tx_id in [tx_id_1, tx_id_2, tx_id_3] { + let transaction = runtime + .block_on(alice_ts_interface.transaction_service_handle.get_any_transaction(tx_id)) + .unwrap() + .unwrap(); + if let WalletTransaction::Completed(tx) = transaction { + if tx_id == tx_id_1 && tx.status == TransactionStatus::FauxUnconfirmed && !found_imported { + found_imported = true; + } + if tx_id == tx_id_2 && tx.status == TransactionStatus::FauxUnconfirmed && !found_faux_unconfirmed { + found_faux_unconfirmed = true; + } + if tx_id == tx_id_3 && tx.status == TransactionStatus::FauxConfirmed && !found_faux_confirmed { + found_faux_confirmed = true; + } } } + if found_imported && found_faux_unconfirmed && found_faux_confirmed { + break; + } } - assert!(found, "Should have found the updated status"); + assert!( + found_imported && found_faux_unconfirmed && found_faux_confirmed, + "Should have found the updated statuses" + ); } diff --git a/base_layer/wallet/tests/transaction_service_tests/storage.rs b/base_layer/wallet/tests/transaction_service_tests/storage.rs index 72b2cf2052..b23b8876ab 100644 --- a/base_layer/wallet/tests/transaction_service_tests/storage.rs +++ b/base_layer/wallet/tests/transaction_service_tests/storage.rs @@ -323,7 +323,7 @@ pub fn test_db_backend(backend: T) { assert!(runtime.block_on(db.fetch_last_mined_transaction()).unwrap().is_none()); runtime - .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, true, 10, [0u8; 16].to_vec(), 5, true)) + .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, true, 10, [0u8; 16].to_vec(), 5, true, false)) .unwrap(); assert_eq!( @@ -596,7 +596,7 @@ async fn import_tx_and_read_it_from_db() { let sqlite_db = TransactionServiceSqliteDatabase::new(connection, Some(cipher)); let transaction = CompletedTransaction::new( - TxId::from(1), + TxId::from(1u64), PublicKey::default(), PublicKey::default(), MicroTari::from(100000), @@ -605,25 +605,94 @@ async fn import_tx_and_read_it_from_db() { Vec::new(), Vec::new(), Vec::new(), - PrivateKey::default(), - PrivateKey::default(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), ), TransactionStatus::Imported, "message".to_string(), Utc::now().naive_utc(), TransactionDirection::Inbound, Some(0), + Some(5), ); sqlite_db .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( - TxId::from(1), + TxId::from(1u64), Box::new(transaction), ))) .unwrap(); - let db_tx = sqlite_db.fetch_imported_transactions().unwrap(); + let transaction = CompletedTransaction::new( + TxId::from(2u64), + PublicKey::default(), + PublicKey::default(), + MicroTari::from(100000), + MicroTari::from(0), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), + ), + TransactionStatus::FauxUnconfirmed, + "message".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + Some(0), + Some(6), + ); + + sqlite_db + .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( + TxId::from(2u64), + Box::new(transaction), + ))) + .unwrap(); + let transaction = CompletedTransaction::new( + TxId::from(3u64), + PublicKey::default(), + PublicKey::default(), + MicroTari::from(100000), + MicroTari::from(0), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + PrivateKey::random(&mut OsRng), + PrivateKey::random(&mut OsRng), + ), + TransactionStatus::FauxConfirmed, + "message".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + Some(0), + Some(7), + ); + + sqlite_db + .write(WriteOperation::Insert(DbKeyValuePair::CompletedTransaction( + TxId::from(3u64), + Box::new(transaction), + ))) + .unwrap(); + + let db_tx = sqlite_db.fetch_imported_transactions().unwrap(); assert_eq!(db_tx.len(), 1); assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(1)); + assert_eq!(db_tx.first().unwrap().mined_height, Some(5)); + + let db_tx = sqlite_db.fetch_unconfirmed_faux_transactions().unwrap(); + assert_eq!(db_tx.len(), 1); + assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(2)); + assert_eq!(db_tx.first().unwrap().mined_height, Some(6)); + + let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(10).unwrap(); + assert_eq!(db_tx.len(), 0); + let db_tx = sqlite_db.fetch_confirmed_faux_transactions_from_height(4).unwrap(); + assert_eq!(db_tx.len(), 1); + assert_eq!(db_tx.first().unwrap().tx_id, TxId::from(3)); + assert_eq!(db_tx.first().unwrap().mined_height, Some(7)); } diff --git a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs index 97f55d1c1e..c84666355f 100644 --- a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs @@ -197,6 +197,7 @@ pub async fn add_transaction_to_database( Utc::now().naive_local(), TransactionDirection::Outbound, coinbase_block_height, + None, ); completed_tx1.valid = valid; db.insert_completed_transaction(tx_id, completed_tx1).await.unwrap(); diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index f122812223..4c56387e0b 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -45,8 +45,10 @@ use std::{panic, path::Path, sync::Arc, time::Duration}; use rand::rngs::OsRng; +use support::{comms_and_services::get_next_memory_address, utils::make_input}; use tari_common_types::{ chain_metadata::ChainMetadata, + transaction::TransactionStatus, types::{PrivateKey, PublicKey}, }; use tari_comms::{ @@ -55,11 +57,14 @@ use tari_comms::{ types::CommsPublicKey, }; use tari_comms_dht::{store_forward::SafConfig, DhtConfig}; -use tari_core::transactions::{ - tari_amount::{uT, MicroTari}, - test_helpers::{create_unblinded_output, TestParams}, - transaction::OutputFeatures, - CryptoFactories, +use tari_core::{ + covenants::Covenant, + transactions::{ + tari_amount::{uT, MicroTari}, + test_helpers::{create_unblinded_output, TestParams}, + transaction::OutputFeatures, + CryptoFactories, + }, }; use tari_crypto::{ inputs, @@ -97,11 +102,7 @@ use tari_wallet::{ }; use tempfile::tempdir; use tokio::{runtime::Runtime, time::sleep}; - pub mod support; -use support::{comms_and_services::get_next_memory_address, utils::make_input}; -use tari_common_types::transaction::TransactionStatus; -use tari_core::covenants::Covenant; use tari_wallet::output_manager_service::storage::database::OutputManagerDatabase; fn create_peer(public_key: CommsPublicKey, net_address: Multiaddr) -> Peer { @@ -763,10 +764,10 @@ async fn test_import_utxo() { .import_utxo( utxo.value, &utxo.spending_key, - script, - input, + script.clone(), + input.clone(), base_node_identity.public_key(), - features, + features.clone(), "Testing".to_string(), utxo.metadata_signature.clone(), &p.script_private_key, diff --git a/base_layer/wallet_ffi/src/callback_handler.rs b/base_layer/wallet_ffi/src/callback_handler.rs index f3a4c7d3b5..4cbaaa5232 100644 --- a/base_layer/wallet_ffi/src/callback_handler.rs +++ b/base_layer/wallet_ffi/src/callback_handler.rs @@ -40,6 +40,12 @@ //! `callback_transaction_mined` - This will be called when a Broadcast transaction is detected as mined via a base //! node request //! +//! `callback_faux_transaction_confirmed` - This will be called when an imported output, recovered output or one-sided +//! transaction is detected as mined +//! +//! `callback_faux_transaction_unconfirmed` - This will be called when a recovered output or one-sided transaction is +//! freshly imported or when an imported transaction transitions from Imported to FauxUnconfirmed +//! //! `callback_discovery_process_complete` - This will be called when a `send_transacion(..)` call is made to a peer //! whose address is not known and a discovery process must be conducted. The outcome of the discovery process is //! relayed via this callback @@ -80,6 +86,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(u64, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), @@ -118,6 +126,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut CompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut CompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut CompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(u64, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(u64, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut CompletedTransaction, u64), @@ -151,6 +161,14 @@ where TBackend: TransactionBackend + 'static target: LOG_TARGET, "TransactionMinedUnconfirmedCallback -> Assigning Fn: {:?}", callback_transaction_mined_unconfirmed ); + info!( + target: LOG_TARGET, + "FauxTransactionConfirmedCallback -> Assigning Fn: {:?}", callback_faux_transaction_confirmed + ); + info!( + target: LOG_TARGET, + "FauxTransactionUnconfirmedCallback -> Assigning Fn: {:?}", callback_faux_transaction_unconfirmed + ); info!( target: LOG_TARGET, "DirectSendResultCallback -> Assigning Fn: {:?}", callback_direct_send_result @@ -191,6 +209,8 @@ where TBackend: TransactionBackend + 'static callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -262,6 +282,14 @@ where TBackend: TransactionBackend + 'static self.receive_transaction_mined_unconfirmed_event(tx_id, num_confirmations).await; self.trigger_balance_refresh().await; }, + TransactionEvent::FauxTransactionConfirmed{tx_id, is_valid: _} => { + self.receive_faux_transaction_confirmed_event(tx_id).await; + self.trigger_balance_refresh().await; + }, + TransactionEvent::FauxTransactionUnconfirmed{tx_id, num_confirmations, is_valid: _} => { + self.receive_faux_transaction_unconfirmed_event(tx_id, num_confirmations).await; + self.trigger_balance_refresh().await; + }, TransactionEvent::TransactionValidationStateChanged(_request_key) => { self.trigger_balance_refresh().await; }, @@ -272,7 +300,7 @@ where TBackend: TransactionBackend + 'static self.transaction_validation_complete_event(request_key.as_u64(), false); }, TransactionEvent::TransactionMinedRequestTimedOut(_tx_id) | - TransactionEvent::TransactionImported(_tx_id) | + TransactionEvent::TransactionImported(_tx_id)| TransactionEvent::TransactionCompletedImmediately(_tx_id) => { self.trigger_balance_refresh().await; @@ -493,7 +521,7 @@ where TBackend: TransactionBackend + 'static Ok(tx) => { debug!( target: LOG_TARGET, - "Calling Received Transaction Mined callback function for TxId: {}", tx_id + "Calling Received Transaction Mined Unconfirmed callback function for TxId: {}", tx_id ); let boxing = Box::into_raw(Box::new(tx)); unsafe { @@ -504,6 +532,38 @@ where TBackend: TransactionBackend + 'static } } + async fn receive_faux_transaction_confirmed_event(&mut self, tx_id: TxId) { + match self.db.get_completed_transaction(tx_id).await { + Ok(tx) => { + debug!( + target: LOG_TARGET, + "Calling Received Faux Transaction Confirmed callback function for TxId: {}", tx_id + ); + let boxing = Box::into_raw(Box::new(tx)); + unsafe { + (self.callback_faux_transaction_confirmed)(boxing); + } + }, + Err(e) => error!(target: LOG_TARGET, "Error retrieving Completed Transaction: {:?}", e), + } + } + + async fn receive_faux_transaction_unconfirmed_event(&mut self, tx_id: TxId, confirmations: u64) { + match self.db.get_completed_transaction(tx_id).await { + Ok(tx) => { + debug!( + target: LOG_TARGET, + "Calling Received Faux Transaction Unconfirmed callback function for TxId: {}", tx_id + ); + let boxing = Box::into_raw(Box::new(tx)); + unsafe { + (self.callback_faux_transaction_unconfirmed)(boxing, confirmations); + } + }, + Err(e) => error!(target: LOG_TARGET, "Error retrieving Completed Transaction: {:?}", e), + } + } + fn transaction_validation_complete_event(&mut self, request_key: u64, success: bool) { debug!( target: LOG_TARGET, diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 64732fc9b1..6d38f44146 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -77,6 +77,8 @@ mod test { pub broadcast_tx_callback_called: bool, pub mined_tx_callback_called: bool, pub mined_tx_unconfirmed_callback_called: u64, + pub faux_tx_confirmed_callback_called: bool, + pub faux_tx_unconfirmed_callback_called: u64, pub direct_send_callback_called: bool, pub store_and_forward_send_callback_called: bool, pub tx_cancellation_callback_called_completed: bool, @@ -98,6 +100,8 @@ mod test { broadcast_tx_callback_called: false, mined_tx_callback_called: false, mined_tx_unconfirmed_callback_called: 0, + faux_tx_confirmed_callback_called: false, + faux_tx_unconfirmed_callback_called: 0, direct_send_callback_called: false, store_and_forward_send_callback_called: false, callback_txo_validation_complete: 0, @@ -158,6 +162,20 @@ mod test { Box::from_raw(tx); } + unsafe extern "C" fn faux_confirmed_callback(tx: *mut CompletedTransaction) { + let mut lock = CALLBACK_STATE.lock().unwrap(); + lock.faux_tx_confirmed_callback_called = true; + drop(lock); + Box::from_raw(tx); + } + + unsafe extern "C" fn faux_unconfirmed_callback(tx: *mut CompletedTransaction, confirmations: u64) { + let mut lock = CALLBACK_STATE.lock().unwrap(); + lock.faux_tx_unconfirmed_callback_called = confirmations; + drop(lock); + Box::from_raw(tx); + } + unsafe extern "C" fn direct_send_callback(_tx_id: u64, _result: bool) { let mut lock = CALLBACK_STATE.lock().unwrap(); lock.direct_send_callback_called = true; @@ -230,6 +248,10 @@ mod test { "1".to_string(), Utc::now().naive_utc(), ); + runtime + .block_on(db.add_pending_inbound_transaction(1u64.into(), inbound_tx.clone())) + .unwrap(); + let completed_tx = CompletedTransaction::new( 2u64.into(), PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), @@ -248,7 +270,12 @@ mod test { Utc::now().naive_utc(), TransactionDirection::Inbound, None, + None, ); + runtime + .block_on(db.insert_completed_transaction(2u64.into(), completed_tx.clone())) + .unwrap(); + let stp = SenderTransactionProtocol::new_placeholder(); let outbound_tx = OutboundTransaction::new( 3u64.into(), @@ -261,33 +288,76 @@ mod test { Utc::now().naive_utc(), false, ); + runtime + .block_on(db.add_pending_outbound_transaction(3u64.into(), outbound_tx.clone())) + .unwrap(); + runtime.block_on(db.cancel_pending_transaction(3u64.into())).unwrap(); + let inbound_tx_cancelled = InboundTransaction { tx_id: 4u64.into(), ..inbound_tx.clone() }; - let completed_tx_cancelled = CompletedTransaction { - tx_id: 5u64.into(), - ..completed_tx.clone() - }; - - runtime - .block_on(db.add_pending_inbound_transaction(1u64.into(), inbound_tx.clone())) - .unwrap(); - runtime - .block_on(db.insert_completed_transaction(2u64.into(), completed_tx.clone())) - .unwrap(); runtime .block_on(db.add_pending_inbound_transaction(4u64.into(), inbound_tx_cancelled)) .unwrap(); runtime.block_on(db.cancel_pending_transaction(4u64.into())).unwrap(); + + let completed_tx_cancelled = CompletedTransaction { + tx_id: 5u64.into(), + ..completed_tx.clone() + }; runtime .block_on(db.insert_completed_transaction(5u64.into(), completed_tx_cancelled.clone())) .unwrap(); runtime.block_on(db.reject_completed_transaction(5u64.into())).unwrap(); + + let faux_unconfirmed_tx = CompletedTransaction::new( + 6u64.into(), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + MicroTari::from(100), + MicroTari::from(2000), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + BlindingFactor::default(), + BlindingFactor::default(), + ), + TransactionStatus::FauxUnconfirmed, + "6".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + None, + Some(2), + ); runtime - .block_on(db.add_pending_outbound_transaction(3u64.into(), outbound_tx.clone())) + .block_on(db.insert_completed_transaction(6u64.into(), faux_unconfirmed_tx.clone())) + .unwrap(); + + let faux_confirmed_tx = CompletedTransaction::new( + 7u64.into(), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + PublicKey::from_secret_key(&PrivateKey::random(&mut OsRng)), + MicroTari::from(100), + MicroTari::from(2000), + Transaction::new( + Vec::new(), + Vec::new(), + Vec::new(), + BlindingFactor::default(), + BlindingFactor::default(), + ), + TransactionStatus::FauxConfirmed, + "7".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Inbound, + None, + Some(5), + ); + runtime + .block_on(db.insert_completed_transaction(7u64.into(), faux_confirmed_tx.clone())) .unwrap(); - runtime.block_on(db.cancel_pending_transaction(3u64.into())).unwrap(); let (transaction_event_sender, transaction_event_receiver) = broadcast::channel(20); let (oms_event_sender, oms_event_receiver) = broadcast::channel(20); @@ -330,6 +400,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + faux_confirmed_callback, + faux_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -478,7 +550,7 @@ mod test { .unwrap(); balance.available_balance -= completed_tx_cancelled.amount; - mock_output_manager_service_state.set_balance(balance); + mock_output_manager_service_state.set_balance(balance.clone()); // Balance updated should be detected with following event, total = 5 times oms_event_sender .send(Arc::new(OutputManagerEvent::TxoValidationSuccess(1u64))) @@ -520,6 +592,51 @@ mod test { .send(Arc::new(TransactionEvent::TransactionValidationCompleted(4u64.into()))) .unwrap(); + balance.pending_incoming_balance += faux_unconfirmed_tx.amount; + mock_output_manager_service_state.set_balance(balance.clone()); + // Balance updated should be detected with following event, total = 6 times + transaction_event_sender + .send(Arc::new(TransactionEvent::FauxTransactionUnconfirmed { + tx_id: 6u64.into(), + num_confirmations: 2, + is_valid: true, + })) + .unwrap(); + let start = Instant::now(); + while start.elapsed().as_secs() < 10 { + { + let lock = CALLBACK_STATE.lock().unwrap(); + if lock.callback_balance_updated == 6 { + callback_balance_updated = 6; + break; + } + } + thread::sleep(Duration::from_millis(100)); + } + assert_eq!(callback_balance_updated, 6); + + balance.available_balance += faux_confirmed_tx.amount; + mock_output_manager_service_state.set_balance(balance.clone()); + // Balance updated should be detected with following event, total = 7 times + transaction_event_sender + .send(Arc::new(TransactionEvent::FauxTransactionConfirmed { + tx_id: 7u64.into(), + is_valid: true, + })) + .unwrap(); + let start = Instant::now(); + while start.elapsed().as_secs() < 10 { + { + let lock = CALLBACK_STATE.lock().unwrap(); + if lock.callback_balance_updated == 7 { + callback_balance_updated = 7; + break; + } + } + thread::sleep(Duration::from_millis(100)); + } + assert_eq!(callback_balance_updated, 7); + dht_event_sender .send(Arc::new(DhtEvent::StoreAndForwardMessagesReceived)) .unwrap(); @@ -541,6 +658,8 @@ mod test { assert!(lock.broadcast_tx_callback_called); assert!(lock.mined_tx_callback_called); assert_eq!(lock.mined_tx_unconfirmed_callback_called, 22u64); + assert!(lock.faux_tx_confirmed_callback_called); + assert_eq!(lock.faux_tx_unconfirmed_callback_called, 2u64); assert!(lock.direct_send_callback_called); assert!(lock.store_and_forward_send_callback_called); assert!(lock.tx_cancellation_callback_called_inbound); @@ -548,7 +667,7 @@ mod test { assert!(lock.tx_cancellation_callback_called_outbound); assert!(lock.saf_messages_received); assert_eq!(lock.callback_txo_validation_complete, 3); - assert_eq!(lock.callback_balance_updated, 5); + assert_eq!(lock.callback_balance_updated, 7); assert_eq!(lock.callback_transaction_validation_complete, 7); assert_eq!(lock.connectivity_status_callback_called, 7); diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 5041dd5084..e957cfc596 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -3231,7 +3231,11 @@ unsafe fn init_logging( /// `callback_transaction_mined` - The callback function pointer matching the function signature. This will be called /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will -/// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be +/// called when a one-sided transaction is detected as mined AND confirmed. +/// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This +/// will be called when a one-sided transaction is detected as mined but not yet confirmed. /// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called /// when a direct send is completed. The first parameter is the transaction id and the second is whether if was /// successful or not. @@ -3293,6 +3297,8 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_broadcast: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_transaction_mined: unsafe extern "C" fn(*mut TariCompletedTransaction), callback_transaction_mined_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), + callback_faux_transaction_confirmed: unsafe extern "C" fn(*mut TariCompletedTransaction), + callback_faux_transaction_unconfirmed: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), callback_direct_send_result: unsafe extern "C" fn(c_ulonglong, bool), callback_store_and_forward_send_result: unsafe extern "C" fn(c_ulonglong, bool), callback_transaction_cancellation: unsafe extern "C" fn(*mut TariCompletedTransaction, u64), @@ -3499,6 +3505,8 @@ pub unsafe extern "C" fn wallet_create( callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -6053,6 +6061,8 @@ mod test { pub broadcast_tx_callback_called: bool, pub mined_tx_callback_called: bool, pub mined_tx_unconfirmed_callback_called: bool, + pub scanned_tx_callback_called: bool, + pub scanned_tx_unconfirmed_callback_called: bool, pub direct_send_callback_called: bool, pub store_and_forward_send_callback_called: bool, pub tx_cancellation_callback_called: bool, @@ -6070,6 +6080,8 @@ mod test { broadcast_tx_callback_called: false, mined_tx_callback_called: false, mined_tx_unconfirmed_callback_called: false, + scanned_tx_callback_called: false, + scanned_tx_unconfirmed_callback_called: false, direct_send_callback_called: false, store_and_forward_send_callback_called: false, tx_cancellation_callback_called: false, @@ -6177,6 +6189,48 @@ mod test { completed_transaction_destroy(tx); } + unsafe extern "C" fn scanned_callback(tx: *mut TariCompletedTransaction) { + assert!(!tx.is_null()); + assert_eq!( + type_of((*tx).clone()), + std::any::type_name::() + ); + assert_eq!((*tx).status, TransactionStatus::FauxConfirmed); + let mut lock = CALLBACK_STATE_FFI.lock().unwrap(); + lock.scanned_tx_callback_called = true; + drop(lock); + completed_transaction_destroy(tx); + } + + unsafe extern "C" fn scanned_unconfirmed_callback(tx: *mut TariCompletedTransaction, _confirmations: u64) { + assert!(!tx.is_null()); + assert_eq!( + type_of((*tx).clone()), + std::any::type_name::() + ); + assert_eq!((*tx).status, TransactionStatus::FauxUnconfirmed); + let mut lock = CALLBACK_STATE_FFI.lock().unwrap(); + lock.scanned_tx_unconfirmed_callback_called = true; + let mut error = 0; + let error_ptr = &mut error as *mut c_int; + let kernel = completed_transaction_get_transaction_kernel(tx, error_ptr); + let excess_hex_ptr = transaction_kernel_get_excess_hex(kernel, error_ptr); + let excess_hex = CString::from_raw(excess_hex_ptr).to_str().unwrap().to_owned(); + assert!(!excess_hex.is_empty()); + let nonce_hex_ptr = transaction_kernel_get_excess_public_nonce_hex(kernel, error_ptr); + let nonce_hex = CString::from_raw(nonce_hex_ptr).to_str().unwrap().to_owned(); + assert!(!nonce_hex.is_empty()); + let sig_hex_ptr = transaction_kernel_get_excess_signature_hex(kernel, error_ptr); + let sig_hex = CString::from_raw(sig_hex_ptr).to_str().unwrap().to_owned(); + assert!(!sig_hex.is_empty()); + string_destroy(excess_hex_ptr as *mut c_char); + string_destroy(sig_hex_ptr as *mut c_char); + string_destroy(nonce_hex_ptr); + transaction_kernel_destroy(kernel); + drop(lock); + completed_transaction_destroy(tx); + } + unsafe extern "C" fn direct_send_callback(_tx_id: c_ulonglong, _result: bool) { // assert!(true); //optimized out by compiler } @@ -6579,6 +6633,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6616,6 +6672,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6719,6 +6777,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6767,6 +6827,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6798,6 +6860,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6824,6 +6888,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6871,6 +6937,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -6947,6 +7015,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -7154,6 +7224,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, @@ -7209,6 +7281,8 @@ mod test { broadcast_callback, mined_callback, mined_unconfirmed_callback, + scanned_callback, + scanned_unconfirmed_callback, direct_send_callback, store_and_forward_send_callback, tx_cancellation_callback, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 889aaea741..cf66c19cbf 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -263,14 +263,19 @@ unsigned long long completed_transaction_get_fee(struct TariCompletedTransaction const char *completed_transaction_get_message(struct TariCompletedTransaction *transaction, int *error_out); // Gets the status of a TariCompletedTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int completed_transaction_get_status(struct TariCompletedTransaction *transaction, int *error_out); // Gets the TransactionID of a TariCompletedTransaction @@ -341,14 +346,19 @@ const char *pending_outbound_transaction_get_message(struct TariPendingOutboundT unsigned long long pending_outbound_transaction_get_timestamp(struct TariPendingOutboundTransaction *transaction, int *error_out); // Gets the status of a TariPendingOutboundTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int pending_outbound_transaction_get_status(struct TariPendingOutboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingOutboundTactions @@ -383,14 +393,19 @@ unsigned long long pending_inbound_transaction_get_amount(struct TariPendingInbo unsigned long long pending_inbound_transaction_get_timestamp(struct TariPendingInboundTransaction *transaction, int *error_out); // Gets the status of a TariPendingInboundTransaction -// | Value | Interpretation | +// | Value | Interpretation | // |---|---| -// | -1 | TxNullError | -// | 0 | Completed | -// | 1 | Broadcast | -// | 2 | Mined | -// | 3 | Imported | -// | 4 | Pending | +// | -1 | TxNullError | +// | 0 | Completed | +// | 1 | Broadcast | +// | 2 | MinedUnconfirmed | +// | 3 | Imported | +// | 4 | Pending | +// | 5 | Coinbase | +// | 6 | MinedConfirmed | +// | 7 | Rejected | +// | 8 | FauxUnconfirmed | +// | 9 | FauxConfirmed | int pending_inbound_transaction_get_status(struct TariPendingInboundTransaction *transaction, int *error_out); // Frees memory for a TariPendingInboundTransaction @@ -451,6 +466,10 @@ struct TariPublicKeys *comms_list_connected_public_keys(struct TariWallet *walle /// when a Broadcast transaction is detected as mined AND confirmed. /// `callback_transaction_mined_unconfirmed` - The callback function pointer matching the function signature. This will /// be called when a Broadcast transaction is detected as mined but not yet confirmed. +/// `callback_faux_transaction_confirmed` - The callback function pointer matching the function signature. This will be called +/// when a one-sided transaction is detected as mined AND confirmed. +/// `callback_faux_transaction_unconfirmed` - The callback function pointer matching the function signature. This will +/// be called when a one-sided transaction is detected as mined but not yet confirmed. /// `callback_direct_send_result` - The callback function pointer matching the function signature. This is called /// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. /// `callback_store_and_forward_send_result` - The callback function pointer matching the function signature. This is called @@ -515,6 +534,8 @@ struct TariWallet *wallet_create(struct TariCommsConfig *config, void (*callback_transaction_broadcast)(struct TariCompletedTransaction *), void (*callback_transaction_mined)(struct TariCompletedTransaction *), void (*callback_transaction_mined_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), + void (*callback_faux_transaction_confirmed)(struct TariCompletedTransaction *), + void (*callback_faux_transaction_unconfirmed)(struct TariCompletedTransaction *, unsigned long long), void (*callback_direct_send_result)(unsigned long long, bool), void (*callback_store_and_forward_send_result)(unsigned long long, bool), void (*callback_transaction_cancellation)(struct TariCompletedTransaction *, unsigned long long), diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index 9e96c20e7d..a00b643873 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -61,7 +61,7 @@ Feature: Wallet FFI And mining node MINER mines 10 blocks Then I wait for wallet SENDER to have at least 1000000 uT And I send 2000000 uT without waiting for broadcast from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And wallet SENDER detects all transactions are at least Broadcast And mining node MINER mines 10 blocks Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT @@ -94,11 +94,11 @@ Feature: Wallet FFI And mining node MINER mines 10 blocks Then I wait for wallet SENDER to have at least 1000000 uT And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 10 blocks Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be Broadcast + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST # The broadcast check does not include delivery; create some holding points to ensure it was received And mining node MINER mines 2 blocks Then all nodes are at height 22 @@ -134,6 +134,7 @@ Feature: Wallet FFI Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT And I stop ffi wallet FFI_WALLET + @critical Scenario: As a client I want to send a one-sided transaction Given I have a seed node SEED And I have a base node BASE1 connected to all seed nodes @@ -143,21 +144,43 @@ Feature: Wallet FFI And I have wallet RECEIVER connected to base node BASE2 And I have mining node MINER connected to base node BASE1 and wallet SENDER And mining node MINER mines 10 blocks - Then I wait for wallet SENDER to have at least 1000000 uT - And I send 2000000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 - Then ffi wallet FFI_WALLET detects AT_LEAST 1 ffi transactions to be Broadcast + Then I wait for wallet SENDER to have at least 5000000 uT + And I send 2400000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 + And I send 2400000 uT from wallet SENDER to wallet FFI_WALLET at fee 20 + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 10 blocks - Then I wait for ffi wallet FFI_WALLET to have at least 1000000 uT + Then I wait for ffi wallet FFI_WALLET to have at least 4000000 uT And I send 1000000 uT from ffi wallet FFI_WALLET to wallet RECEIVER at fee 20 via one-sided transactions + Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 2 blocks Then all nodes are at height 22 - And mining node MINER mines 2 blocks - Then all nodes are at height 24 - And mining node MINER mines 6 blocks - Then I wait for wallet RECEIVER to have at least 1000000 uT - Then I wait for ffi wallet FFI_WALLET to receive 2 mined + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and valid + And mining node MINER mines 5 blocks + Then all nodes are at height 27 + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid And I stop ffi wallet FFI_WALLET + @critical + Scenario: As a client I want to receive a one-sided transaction + Given I have a seed node SEED + And I have a base node BASE1 connected to all seed nodes + And I have a base node BASE2 connected to all seed nodes + And I have wallet SENDER connected to base node BASE1 + And I have a ffi wallet FFI_RECEIVER connected to base node BASE2 + And I have mining node MINER connected to base node BASE1 and wallet SENDER + And mining node MINER mines 10 blocks + Then I wait for wallet SENDER to have at least 5000000 uT + Then I send a one-sided transaction of 1000000 uT from SENDER to FFI_RECEIVER at fee 20 + And mining node MINER mines 2 blocks + Then all nodes are at height 12 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_FAUX_UNCONFIRMED + And I send 1000000 uT from wallet SENDER to wallet FFI_RECEIVER at fee 20 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_BROADCAST + And mining node MINER mines 5 blocks + Then all nodes are at height 17 + Then ffi wallet FFI_RECEIVER detects AT_LEAST 1 ffi transactions to be TRANSACTION_STATUS_FAUX_CONFIRMED + And I stop ffi wallet FFI_RECEIVER + # Scenario: As a client I want to get my balance # It's a subtest of "As a client I want to retrieve a list of transactions I have made and received" diff --git a/integration_tests/features/support/ffi_steps.js b/integration_tests/features/support/ffi_steps.js index 1853b77eda..7ecb6459ed 100644 --- a/integration_tests/features/support/ffi_steps.js +++ b/integration_tests/features/support/ffi_steps.js @@ -3,16 +3,6 @@ const expect = require("chai").expect; const { sleep, waitForIterate } = require("../../helpers/util"); -When( - "I have ffi wallet {word} connected to base node {word}", - { timeout: 20 * 1000 }, - async function (name, node) { - let wallet = await this.createAndAddFFIWallet(name); - let peer = this.nodes[node].peerAddress().split("::"); - wallet.addBaseNodePeer(peer[0], peer[1]); - } -); - Then("I want to get emoji id of ffi wallet {word}", async function (name) { let wallet = this.getWallet(name); let emoji_id = wallet.identifyEmoji(); @@ -479,13 +469,21 @@ Then( ); Then( - "ffi wallet {word} detects {word} {int} ffi transactions to be Broadcast", + "ffi wallet {word} detects {word} {int} ffi transactions to be {word}", { timeout: 125 * 1000 }, - async function (walletName, comparison, amount) { + async function (walletName, comparison, amount, status) { // Pending -> Completed -> Broadcast -> Mined Unconfirmed -> Mined Confirmed const atLeast = "AT_LEAST"; const exactly = "EXACTLY"; expect(comparison === atLeast || comparison === exactly).to.equal(true); + const broadcast = "TRANSACTION_STATUS_BROADCAST"; + const fauxUnconfirmed = "TRANSACTION_STATUS_FAUX_UNCONFIRMED"; + const fauxConfirmed = "TRANSACTION_STATUS_FAUX_CONFIRMED"; + expect( + status === broadcast || + status === fauxUnconfirmed || + status === fauxConfirmed + ).to.equal(true); const wallet = this.getWallet(walletName); console.log("\n"); @@ -496,27 +494,53 @@ Then( comparison + " " + amount + - " broadcast transaction(s)" + " " + + status + + " transaction(s)" ); await waitForIterate( () => { - return wallet.getCounters().broadcast >= amount; + switch (status) { + case broadcast: + return wallet.getCounters().broadcast >= amount; + case fauxUnconfirmed: + return wallet.getCounters().fauxUnconfirmed >= amount; + case fauxConfirmed: + return wallet.getCounters().fauxConfirmed >= amount; + default: + expect(status).to.equal("please add this<< TransactionStatus"); + } }, true, 1000, 120 ); - if (!(wallet.getCounters().broadcast >= amount)) { - console.log("Counter not adequate!"); + let amountOfCallbacks; + switch (status) { + case broadcast: + amountOfCallbacks = wallet.getCounters().broadcast; + break; + case fauxUnconfirmed: + amountOfCallbacks = wallet.getCounters().fauxUnconfirmed; + break; + case fauxConfirmed: + amountOfCallbacks = wallet.getCounters().fauxConfirmed; + break; + default: + expect(status).to.equal("please add this<< TransactionStatus"); + } + + if (!(amountOfCallbacks >= amount)) { + console.log("\nCounter not adequate!", wallet.getCounters()); } else { console.log(wallet.getCounters()); } if (comparison === atLeast) { - expect(wallet.getCounters().broadcast >= amount).to.equal(true); + expect(amountOfCallbacks >= amount).to.equal(true); } else { - expect(wallet.getCounters().broadcast === amount).to.equal(true); + expect(amountOfCallbacks === amount).to.equal(true); } } ); diff --git a/integration_tests/features/support/wallet_steps.js b/integration_tests/features/support/wallet_steps.js index 1828e32fc4..16e80af104 100644 --- a/integration_tests/features/support/wallet_steps.js +++ b/integration_tests/features/support/wallet_steps.js @@ -2034,7 +2034,9 @@ Then( 1 + " has " + transactions[i]["status"] + - " and is valid(" + + " (need " + + transactionStatus + + ") and is valid(" + transactions[i]["valid"] + ")" ); diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index 2f2f455fa2..cb98d17bf4 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -292,6 +292,8 @@ class InterfaceFFI { this.ptr, this.ptr, this.ptr, + this.ptr, + this.ptr, this.boolPtr, this.intPtr, ], @@ -1136,6 +1138,14 @@ class InterfaceFFI { return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); } + static createCallbackFauxTransactionConfirmed(fn) { + return ffi.Callback(this.void, [this.ptr], fn); + } + + static createCallbackFauxTransactionUnconfirmed(fn) { + return ffi.Callback(this.void, [this.ptr, this.ulonglong], fn); + } + static createCallbackDirectSendResult(fn) { return ffi.Callback(this.void, [this.ulonglong, this.bool], fn); } @@ -1184,6 +1194,8 @@ class InterfaceFFI { callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, @@ -1209,6 +1221,8 @@ class InterfaceFFI { callback_transaction_broadcast, callback_transaction_mined, callback_transaction_mined_unconfirmed, + callback_faux_transaction_confirmed, + callback_faux_transaction_unconfirmed, callback_direct_send_result, callback_store_and_forward_send_result, callback_transaction_cancellation, diff --git a/integration_tests/helpers/ffi/wallet.js b/integration_tests/helpers/ffi/wallet.js index d307660dd2..fc19fda279 100644 --- a/integration_tests/helpers/ffi/wallet.js +++ b/integration_tests/helpers/ffi/wallet.js @@ -22,11 +22,16 @@ class Wallet { ptr; balance = new WalletBalance(); log_path = ""; - receivedTransaction = 0; - receivedTransactionReply = 0; + transactionReceived = 0; + transactionReplyReceived = 0; transactionBroadcast = 0; transactionMined = 0; - saf_messages = 0; + transactionMinedUnconfirmed = 0; + transactionFauxConfirmed = 0; + transactionFauxUnconfirmed = 0; + transactionSafMessageReceived = 0; + transactionCancelled = 0; + transactionFinalized = 0; txo_validation_complete = false; txo_validation_result = 0; tx_validation_complete = false; @@ -37,6 +42,8 @@ class Wallet { callback_transaction_broadcast; callback_transaction_mined; callback_transaction_mined_unconfirmed; + callback_faux_transaction_confirmed; + callback_faux_transaction_unconfirmed; callback_direct_send_result; callback_store_and_forward_send_result; callback_transaction_cancellation; @@ -61,27 +68,31 @@ class Wallet { } clearCallbackCounters() { - this.receivedTransaction = - this.receivedTransactionReply = + this.transactionReceived = + this.transactionReplyReceived = this.transactionBroadcast = this.transactionMined = - this.saf_messages = - this.cancelled = - this.minedunconfirmed = - this.finalized = + this.transactionFauxConfirmed = + this.transactionSafMessageReceived = + this.transactionCancelled = + this.transactionMinedUnconfirmed = + this.transactionFauxUnconfirmed = + this.transactionFinalized = 0; } getCounters() { return { - received: this.receivedTransaction, - replyreceived: this.receivedTransactionReply, + received: this.transactionReceived, + replyReceived: this.transactionReplyReceived, broadcast: this.transactionBroadcast, - finalized: this.finalized, - minedunconfirmed: this.minedunconfirmed, - cancelled: this.cancelled, + finalized: this.transactionFinalized, + minedUnconfirmed: this.transactionMinedUnconfirmed, + fauxUnconfirmed: this.transactionFauxUnconfirmed, + cancelled: this.transactionCancelled, mined: this.transactionMined, - saf: this.saf_messages, + fauxConfirmed: this.transactionFauxConfirmed, + saf: this.transactionSafMessageReceived, }; } @@ -116,6 +127,14 @@ class Wallet { InterfaceFFI.createCallbackTransactionMinedUnconfirmed( this.onTransactionMinedUnconfirmed ); + this.callback_faux_transaction_confirmed = + InterfaceFFI.createCallbackFauxTransactionConfirmed( + this.onFauxTransactionConfirmed + ); + this.callback_faux_transaction_unconfirmed = + InterfaceFFI.createCallbackFauxTransactionUnconfirmed( + this.onFauxTransactionUnconfirmed + ); this.callback_direct_send_result = InterfaceFFI.createCallbackDirectSendResult(this.onDirectSendResult); this.callback_store_and_forward_send_result = @@ -148,14 +167,16 @@ class Wallet { ); //endregion - this.receivedTransaction = 0; - this.receivedTransactionReply = 0; + this.transactionReceived = 0; + this.transactionReplyReceived = 0; this.transactionBroadcast = 0; this.transactionMined = 0; - this.saf_messages = 0; - this.cancelled = 0; - this.minedunconfirmed = 0; - this.finalized = 0; + this.transactionFauxConfirmed = 0; + this.transactionSafMessageReceived = 0; + this.transactionCancelled = 0; + this.transactionMinedUnconfirmed = 0; + this.transactionFauxUnconfirmed = 0; + this.transactionFinalized = 0; this.recoveryFinished = true; let sanitize = null; let words = null; @@ -179,6 +200,8 @@ class Wallet { this.callback_transaction_broadcast, this.callback_transaction_mined, this.callback_transaction_mined_unconfirmed, + this.callback_faux_transaction_confirmed, + this.callback_faux_transaction_unconfirmed, this.callback_direct_send_result, this.callback_store_and_forward_send_result, this.callback_transaction_cancellation, @@ -198,7 +221,7 @@ class Wallet { `${new Date().toISOString()} received Transaction with txID ${tx.getTransactionID()}` ); tx.destroy(); - this.receivedTransaction += 1; + this.transactionReceived += 1; }; onReceivedTransactionReply = (ptr) => { @@ -208,7 +231,7 @@ class Wallet { `${new Date().toISOString()} received reply for Transaction with txID ${tx.getTransactionID()}.` ); tx.destroy(); - this.receivedTransactionReply += 1; + this.transactionReplyReceived += 1; }; onReceivedFinalizedTransaction = (ptr) => { @@ -218,7 +241,7 @@ class Wallet { `${new Date().toISOString()} received finalization for Transaction with txID ${tx.getTransactionID()}.` ); tx.destroy(); - this.finalized += 1; + this.transactionFinalized += 1; }; onTransactionBroadcast = (ptr) => { @@ -248,7 +271,27 @@ class Wallet { `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} is mined unconfirmed with ${confirmations} confirmations.` ); tx.destroy(); - this.minedunconfirmed += 1; + this.transactionMinedUnconfirmed += 1; + }; + + onFauxTransactionConfirmed = (ptr) => { + let tx = new CompletedTransaction(); + tx.pointerAssign(ptr); + console.log( + `${new Date().toISOString()} Faux transaction with txID ${tx.getTransactionID()} was confirmed.` + ); + tx.destroy(); + this.transactionFauxConfirmed += 1; + }; + + onFauxTransactionUnconfirmed = (ptr, confirmations) => { + let tx = new CompletedTransaction(); + tx.pointerAssign(ptr); + console.log( + `${new Date().toISOString()} Faux transaction with txID ${tx.getTransactionID()} is unconfirmed with ${confirmations} confirmations.` + ); + tx.destroy(); + this.transactionFauxUnconfirmed += 1; }; onTransactionCancellation = (ptr, reason) => { @@ -258,7 +301,7 @@ class Wallet { `${new Date().toISOString()} Transaction with txID ${tx.getTransactionID()} was cancelled with reason code ${reason}.` ); tx.destroy(); - this.cancelled += 1; + this.transactionCancelled += 1; }; onDirectSendResult = (id, success) => { @@ -308,7 +351,7 @@ class Wallet { onSafMessageReceived = () => { console.log(`${new Date().toISOString()} callbackSafMessageReceived()`); - this.saf_messages += 1; + this.transactionSafMessageReceived += 1; }; onRecoveryProgress = (a, b, c) => { @@ -465,6 +508,8 @@ class Wallet { this.callback_transaction_broadcast = this.callback_transaction_mined = this.callback_transaction_mined_unconfirmed = + this.callback_faux_transaction_confirmed = + this.callback_faux_transaction_unconfirmed = this.callback_direct_send_result = this.callback_store_and_forward_send_result = this.callback_transaction_cancellation = diff --git a/integration_tests/helpers/walletFFIClient.js b/integration_tests/helpers/walletFFIClient.js index 6bfd907f12..bc1b230d0c 100644 --- a/integration_tests/helpers/walletFFIClient.js +++ b/integration_tests/helpers/walletFFIClient.js @@ -157,7 +157,7 @@ class WalletFFIClient { seed_words_text, pass_phrase, rolling_log_files = 50, - byte_size_per_log = 102400 + byte_size_per_log = 1048576 ) { this.pass_phrase = pass_phrase; if (seed_words_text) { diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 868ccd2cda..63d7981e34 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -653,7 +653,7 @@ }, "any-promise": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", "dev": true }, @@ -747,7 +747,7 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, @@ -759,7 +759,7 @@ }, "assertion-error-formatter": { "version": "3.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", "dev": true, "requires": { @@ -993,7 +993,7 @@ }, "colors": { "version": "1.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { @@ -1088,7 +1088,7 @@ }, "d": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, "requires": { @@ -1152,7 +1152,7 @@ }, "diff": { "version": "4.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, @@ -1167,7 +1167,7 @@ }, "duration": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz", "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", "dev": true, "requires": { @@ -1259,7 +1259,7 @@ }, "es5-ext": { "version": "0.10.53", - "resolved": false, + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { @@ -1270,7 +1270,7 @@ }, "es6-iterator": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { @@ -1281,7 +1281,7 @@ }, "es6-symbol": { "version": "3.1.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { @@ -1704,7 +1704,7 @@ }, "ext": { "version": "1.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dev": true, "requires": { @@ -1713,7 +1713,7 @@ "dependencies": { "type": { "version": "2.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } @@ -1765,7 +1765,7 @@ }, "figures": { "version": "3.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" @@ -1946,7 +1946,7 @@ }, "globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "resolved": false, "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, @@ -2042,7 +2042,7 @@ }, "indent-string": { "version": "4.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, @@ -2175,7 +2175,7 @@ }, "is-stream": { "version": "2.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, @@ -2252,7 +2252,7 @@ }, "jsesc": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "resolved": false, "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, @@ -2328,7 +2328,7 @@ }, "knuth-shuffle-seeded": { "version": "1.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", "integrity": "sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=", "dev": true, "requires": { @@ -2504,7 +2504,7 @@ }, "mz": { "version": "2.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "requires": { @@ -2521,7 +2521,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -2574,7 +2574,7 @@ }, "object-assign": { "version": "4.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { @@ -2669,7 +2669,7 @@ }, "pad-right": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=", "dev": true, "requires": { @@ -2877,7 +2877,7 @@ }, "repeat-string": { "version": "1.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, @@ -2942,7 +2942,7 @@ }, "seed-random": { "version": "2.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", "dev": true }, @@ -3038,7 +3038,7 @@ }, "source-map": { "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "resolved": false, "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, @@ -3068,7 +3068,7 @@ }, "stack-chain": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, @@ -3118,7 +3118,7 @@ }, "string-argv": { "version": "0.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, @@ -3263,7 +3263,7 @@ }, "thenify": { "version": "3.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { @@ -3272,7 +3272,7 @@ }, "thenify-all": { "version": "1.6.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "dev": true, "requires": { @@ -3290,7 +3290,7 @@ }, "to-fast-properties": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "resolved": false, "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, @@ -3336,7 +3336,7 @@ }, "type": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, @@ -3398,7 +3398,7 @@ }, "util-arity": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", "integrity": "sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=", "dev": true }, @@ -3440,104 +3440,68 @@ "dependencies": { "@grpc/grpc-js": { "version": "1.3.6", - "resolved": false, - "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", "requires": { "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { "version": "0.5.6", - "resolved": false, - "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" } }, "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "version": "1.1.2" }, "@protobufjs/base64": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "version": "1.1.2" }, "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": false, - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "version": "2.0.4" }, "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "version": "1.1.0" }, "@protobufjs/fetch": { "version": "1.1.0", - "resolved": false, - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "@protobufjs/float": { - "version": "1.0.2", - "resolved": false, - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "version": "1.0.2" }, "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "version": "1.1.0" }, "@protobufjs/path": { - "version": "1.1.2", - "resolved": false, - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "version": "1.1.2" }, "@protobufjs/pool": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "version": "1.1.0" }, "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": false, - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "version": "1.1.0" }, "@types/long": { - "version": "4.0.1", - "resolved": false, - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.1" }, "@types/node": { - "version": "16.3.2", - "resolved": false, - "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" + "version": "16.3.2" }, "grpc-promise": { - "version": "1.4.0", - "resolved": false, - "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" + "version": "1.4.0" }, "lodash.camelcase": { - "version": "4.3.0", - "resolved": false, - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "version": "4.3.0" }, "long": { - "version": "4.0.0", - "resolved": false, - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "version": "4.0.0" }, "protobufjs": { "version": "6.11.2", - "resolved": false, - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", From d4565af19cf909565018709e3a107acb65abd88c Mon Sep 17 00:00:00 2001 From: Mike the Tike Date: Thu, 10 Feb 2022 12:24:26 +0200 Subject: [PATCH 05/28] v0.28.0 --- Cargo.lock | 74 +++++++++---------- applications/launchpad/backend/Cargo.toml | 6 +- applications/tari_app_grpc/Cargo.toml | 4 +- applications/tari_app_utilities/Cargo.toml | 2 +- applications/tari_base_node/Cargo.toml | 2 +- applications/tari_console_wallet/Cargo.toml | 2 +- .../tari_merge_mining_proxy/Cargo.toml | 2 +- applications/tari_mining_node/Cargo.toml | 2 +- .../tari_stratum_transcoder/Cargo.toml | 2 +- applications/tari_validator_node/Cargo.toml | 2 +- applications/test_faucet/Cargo.toml | 2 +- base_layer/common_types/Cargo.toml | 2 +- base_layer/core/Cargo.toml | 30 ++++---- base_layer/key_manager/Cargo.toml | 4 +- base_layer/mmr/Cargo.toml | 2 +- base_layer/p2p/Cargo.toml | 18 ++--- base_layer/service_framework/Cargo.toml | 6 +- base_layer/tari_stratum_ffi/Cargo.toml | 4 +- base_layer/wallet/Cargo.toml | 26 +++---- base_layer/wallet_ffi/Cargo.toml | 22 +++--- changelog.md | 38 ++++++++++ common/Cargo.toml | 6 +- common_sqlite/Cargo.toml | 2 +- comms/Cargo.toml | 10 +-- comms/dht/Cargo.toml | 14 ++-- comms/rpc_macros/Cargo.toml | 6 +- infrastructure/derive/Cargo.toml | 2 +- infrastructure/libtor/Cargo.toml | 2 +- infrastructure/shutdown/Cargo.toml | 2 +- infrastructure/storage/Cargo.toml | 2 +- infrastructure/test_utils/Cargo.toml | 2 +- package-lock.json | 2 +- package.json | 1 - 33 files changed, 170 insertions(+), 133 deletions(-) delete mode 100644 package.json diff --git a/Cargo.lock b/Cargo.lock index 5d000704c4..0bbc0b3c11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6446,7 +6446,7 @@ dependencies = [ [[package]] name = "tari_app_grpc" -version = "0.27.3" +version = "0.28.0" dependencies = [ "chrono", "prost", @@ -6462,7 +6462,7 @@ dependencies = [ [[package]] name = "tari_app_utilities" -version = "0.27.3" +version = "0.28.0" dependencies = [ "config 0.9.3", "dirs-next 1.0.2", @@ -6484,7 +6484,7 @@ dependencies = [ [[package]] name = "tari_base_node" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "bincode", @@ -6579,7 +6579,7 @@ dependencies = [ [[package]] name = "tari_common" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "config 0.9.3", @@ -6597,7 +6597,7 @@ dependencies = [ "sha2", "structopt", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tempfile", "thiserror", "toml 0.5.8", @@ -6605,7 +6605,7 @@ dependencies = [ [[package]] name = "tari_common_sqlite" -version = "0.27.3" +version = "0.28.0" dependencies = [ "diesel", "log", @@ -6614,7 +6614,7 @@ dependencies = [ [[package]] name = "tari_common_types" -version = "0.27.3" +version = "0.28.0" dependencies = [ "digest 0.9.0", "lazy_static 1.4.0", @@ -6628,7 +6628,7 @@ dependencies = [ [[package]] name = "tari_comms" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "async-trait", @@ -6664,7 +6664,7 @@ dependencies = [ "tari_metrics", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tempfile", "thiserror", "tokio 1.16.1", @@ -6677,7 +6677,7 @@ dependencies = [ [[package]] name = "tari_comms_dht" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -6711,7 +6711,7 @@ dependencies = [ "tari_crypto", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tari_utilities", "tempfile", "thiserror", @@ -6722,7 +6722,7 @@ dependencies = [ [[package]] name = "tari_comms_rpc_macros" -version = "0.27.3" +version = "0.28.0" dependencies = [ "futures 0.3.21", "proc-macro2", @@ -6730,14 +6730,14 @@ dependencies = [ "quote", "syn", "tari_comms", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tokio 1.16.1", "tower-service", ] [[package]] name = "tari_console_wallet" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bitflags 1.3.2", "chrono", @@ -6782,7 +6782,7 @@ dependencies = [ [[package]] name = "tari_core" -version = "0.27.3" +version = "0.28.0" dependencies = [ "async-trait", "bincode", @@ -6827,7 +6827,7 @@ dependencies = [ "tari_service_framework", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tari_utilities", "tempfile", "thiserror", @@ -6929,7 +6929,7 @@ dependencies = [ [[package]] name = "tari_key_manager" -version = "0.27.3" +version = "0.28.0" dependencies = [ "argon2", "arrayvec 0.7.2", @@ -6958,7 +6958,7 @@ dependencies = [ [[package]] name = "tari_launchpad" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bollard", "config 0.11.0", @@ -6996,7 +6996,7 @@ dependencies = [ [[package]] name = "tari_merge_mining_proxy" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "bincode", @@ -7045,7 +7045,7 @@ dependencies = [ [[package]] name = "tari_mining_node" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bufstream", "chrono", @@ -7075,7 +7075,7 @@ dependencies = [ [[package]] name = "tari_mmr" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bincode", "blake2", @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "tari_p2p" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "bytes 0.5.6", @@ -7120,7 +7120,7 @@ dependencies = [ "tari_service_framework", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tari_utilities", "tempfile", "thiserror", @@ -7134,7 +7134,7 @@ dependencies = [ [[package]] name = "tari_service_framework" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "async-trait", @@ -7142,7 +7142,7 @@ dependencies = [ "futures-test", "log", "tari_shutdown", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "thiserror", "tokio 1.16.1", "tower", @@ -7151,7 +7151,7 @@ dependencies = [ [[package]] name = "tari_shutdown" -version = "0.27.3" +version = "0.28.0" dependencies = [ "futures 0.3.21", "tokio 1.16.1", @@ -7159,7 +7159,7 @@ dependencies = [ [[package]] name = "tari_storage" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bincode", "lmdb-zero", @@ -7173,7 +7173,7 @@ dependencies = [ [[package]] name = "tari_stratum_ffi" -version = "0.27.3" +version = "0.28.0" dependencies = [ "hex", "libc", @@ -7189,7 +7189,7 @@ dependencies = [ [[package]] name = "tari_stratum_transcoder" -version = "0.27.3" +version = "0.28.0" dependencies = [ "bincode", "bytes 0.5.6", @@ -7236,7 +7236,7 @@ dependencies = [ [[package]] name = "tari_test_utils" -version = "0.27.3" +version = "0.28.0" dependencies = [ "futures 0.3.21", "futures-test", @@ -7266,7 +7266,7 @@ dependencies = [ [[package]] name = "tari_validator_node" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "async-trait", @@ -7299,7 +7299,7 @@ dependencies = [ "tari_service_framework", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "thiserror", "tokio 1.16.1", "tokio-stream", @@ -7308,7 +7308,7 @@ dependencies = [ [[package]] name = "tari_wallet" -version = "0.27.3" +version = "0.28.0" dependencies = [ "aes-gcm 0.8.0", "argon2", @@ -7347,7 +7347,7 @@ dependencies = [ "tari_service_framework", "tari_shutdown", "tari_storage", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tari_utilities", "tempfile", "thiserror", @@ -7357,7 +7357,7 @@ dependencies = [ [[package]] name = "tari_wallet_ffi" -version = "0.27.3" +version = "0.28.0" dependencies = [ "chrono", "futures 0.3.21", @@ -7377,7 +7377,7 @@ dependencies = [ "tari_p2p", "tari_service_framework", "tari_shutdown", - "tari_test_utils 0.27.3", + "tari_test_utils 0.28.0", "tari_utilities", "tari_wallet", "tempfile", @@ -7569,7 +7569,7 @@ dependencies = [ [[package]] name = "test_faucet" -version = "0.27.3" +version = "0.28.0" dependencies = [ "rand 0.8.4", "serde 1.0.136", diff --git a/applications/launchpad/backend/Cargo.toml b/applications/launchpad/backend/Cargo.toml index 162bc04036..8480e0530f 100644 --- a/applications/launchpad/backend/Cargo.toml +++ b/applications/launchpad/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_launchpad" -version = "0.27.3" +version = "0.28.0" description = "The Tari Launcher" authors = ["The Tari Development Community"] license = "" @@ -14,8 +14,8 @@ build = "src/build.rs" tauri-build = { version = "1.0.0-beta.4" } [dependencies] -tari_app_utilities = { version = "^0.27", path = "../../tari_app_utilities" } -tari_comms = { version = "^0.27", path = "../../../comms" } +tari_app_utilities = { version = "^0.28", path = "../../tari_app_utilities" } +tari_comms = { version = "^0.28", path = "../../../comms" } bollard = "0.11.0" config = "0.11.0" env_logger = "0.9.0" diff --git a/applications/tari_app_grpc/Cargo.toml b/applications/tari_app_grpc/Cargo.toml index b58d68feb3..3b1181b0dd 100644 --- a/applications/tari_app_grpc/Cargo.toml +++ b/applications/tari_app_grpc/Cargo.toml @@ -4,11 +4,11 @@ authors = ["The Tari Development Community"] description = "This crate is to provide a single source for all cross application grpc files and conversions to and from tari::core" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] -tari_common_types = { version = "^0.27", path = "../../base_layer/common_types"} +tari_common_types = { version = "^0.28", path = "../../base_layer/common_types"} tari_core = { path = "../../base_layer/core"} tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } tari_comms = { path = "../../comms"} diff --git a/applications/tari_app_utilities/Cargo.toml b/applications/tari_app_utilities/Cargo.toml index 01ab2fec14..97883782be 100644 --- a/applications/tari_app_utilities/Cargo.toml +++ b/applications/tari_app_utilities/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_app_utilities" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] edition = "2018" diff --git a/applications/tari_base_node/Cargo.toml b/applications/tari_base_node/Cargo.toml index 1f6582cef5..e661d371ce 100644 --- a/applications/tari_base_node/Cargo.toml +++ b/applications/tari_base_node/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The tari full base node implementation" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index 257f2a9f26..351f044d85 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_console_wallet" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] edition = "2018" diff --git a/applications/tari_merge_mining_proxy/Cargo.toml b/applications/tari_merge_mining_proxy/Cargo.toml index 7cdd0751dc..29e99352ea 100644 --- a/applications/tari_merge_mining_proxy/Cargo.toml +++ b/applications/tari_merge_mining_proxy/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The tari merge miner proxy for xmrig" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [features] diff --git a/applications/tari_mining_node/Cargo.toml b/applications/tari_mining_node/Cargo.toml index a5d18a2995..3d3657c9bd 100644 --- a/applications/tari_mining_node/Cargo.toml +++ b/applications/tari_mining_node/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The tari mining node implementation" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] diff --git a/applications/tari_stratum_transcoder/Cargo.toml b/applications/tari_stratum_transcoder/Cargo.toml index 5d2fc2b6ac..2d9b6be387 100644 --- a/applications/tari_stratum_transcoder/Cargo.toml +++ b/applications/tari_stratum_transcoder/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The tari stratum transcoder for miningcore" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [features] diff --git a/applications/tari_validator_node/Cargo.toml b/applications/tari_validator_node/Cargo.toml index d64d87a09d..27159e25b9 100644 --- a/applications/tari_validator_node/Cargo.toml +++ b/applications/tari_validator_node/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "The Tari validator node implementation" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] diff --git a/applications/test_faucet/Cargo.toml b/applications/test_faucet/Cargo.toml index e33f65e746..99f802dac6 100644 --- a/applications/test_faucet/Cargo.toml +++ b/applications/test_faucet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "test_faucet" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] edition = "2018" diff --git a/base_layer/common_types/Cargo.toml b/base_layer/common_types/Cargo.toml index 1720efae0d..f02d26abf9 100644 --- a/base_layer/common_types/Cargo.toml +++ b/base_layer/common_types/Cargo.toml @@ -3,7 +3,7 @@ name = "tari_common_types" authors = ["The Tari Development Community"] description = "Tari cryptocurrency common types" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] diff --git a/base_layer/core/Cargo.toml b/base_layer/core/Cargo.toml index 5a7bc7c61c..998d9ca35a 100644 --- a/base_layer/core/Cargo.toml +++ b/base_layer/core/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [features] @@ -18,19 +18,19 @@ base_node_proto = [] avx2 = ["tari_crypto/avx2"] [dependencies] -tari_common = { version = "^0.27", path = "../../common" } -tari_common_types = { version = "^0.27", path = "../../base_layer/common_types" } -tari_comms = { version = "^0.27", path = "../../comms" } -tari_comms_dht = { version = "^0.27", path = "../../comms/dht" } -tari_comms_rpc_macros = { version = "^0.27", path = "../../comms/rpc_macros" } +tari_common = { version = "^0.28", path = "../../common" } +tari_common_types = { version = "^0.28", path = "../../base_layer/common_types" } +tari_comms = { version = "^0.28", path = "../../comms" } +tari_comms_dht = { version = "^0.28", path = "../../comms/dht" } +tari_comms_rpc_macros = { version = "^0.28", path = "../../comms/rpc_macros" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } tari_metrics = { path = "../../infrastructure/metrics" } -tari_mmr = { version = "^0.27", path = "../../base_layer/mmr", optional = true, features = ["native_bitmap"] } -tari_p2p = { version = "^0.27", path = "../../base_layer/p2p" } -tari_service_framework = { version = "^0.27", path = "../service_framework" } -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } -tari_storage = { version = "^0.27", path = "../../infrastructure/storage" } -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_mmr = { version = "^0.28", path = "../../base_layer/mmr", optional = true, features = ["native_bitmap"] } +tari_p2p = { version = "^0.28", path = "../../base_layer/p2p" } +tari_service_framework = { version = "^0.28", path = "../service_framework" } +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } +tari_storage = { version = "^0.28", path = "../../infrastructure/storage" } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } tari_utilities = "0.3.0" async-trait = "0.1.50" @@ -69,12 +69,12 @@ tracing-attributes = "*" uint = { version = "0.9", default-features = false } [dev-dependencies] -tari_p2p = { version = "^0.27", path = "../../base_layer/p2p", features = ["test-mocks"] } -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_p2p = { version = "^0.28", path = "../../base_layer/p2p", features = ["test-mocks"] } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } config = { version = "0.9.3" } env_logger = "0.7.0" tempfile = "3.1.0" [build-dependencies] -tari_common = { version = "^0.27", path = "../../common", features = ["build"] } +tari_common = { version = "^0.28", path = "../../common", features = ["build"] } diff --git a/base_layer/key_manager/Cargo.toml b/base_layer/key_manager/Cargo.toml index cd281d6ecf..b525d943e1 100644 --- a/base_layer/key_manager/Cargo.toml +++ b/base_layer/key_manager/Cargo.toml @@ -4,14 +4,14 @@ authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet key management" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2021" [lib] crate-type = ["lib", "cdylib"] [dependencies] -tari_common_types = { version = "^0.27", path = "../../base_layer/common_types" } +tari_common_types = { version = "^0.28", path = "../../base_layer/common_types" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } arrayvec = "0.7.1" diff --git a/base_layer/mmr/Cargo.toml b/base_layer/mmr/Cargo.toml index 0965328e9d..665567f2a4 100644 --- a/base_layer/mmr/Cargo.toml +++ b/base_layer/mmr/Cargo.toml @@ -4,7 +4,7 @@ authors = ["The Tari Development Community"] description = "A Merkle Mountain Range implementation" repository = "https://github.com/tari-project/tari" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [features] diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index 2b7f079bd6..f2ade27176 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_p2p" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development community"] description = "Tari base layer-specific peer-to-peer communication features" repository = "https://github.com/tari-project/tari" @@ -10,13 +10,13 @@ license = "BSD-3-Clause" edition = "2018" [dependencies] -tari_comms = { version = "^0.27", path = "../../comms" } -tari_comms_dht = { version = "^0.27", path = "../../comms/dht" } -tari_common = { version = "^0.27", path = "../../common" } +tari_comms = { version = "^0.28", path = "../../comms" } +tari_comms_dht = { version = "^0.28", path = "../../comms/dht" } +tari_common = { version = "^0.28", path = "../../common" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } -tari_service_framework = { version = "^0.27", path = "../service_framework" } -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } -tari_storage = { version = "^0.27", path = "../../infrastructure/storage" } +tari_service_framework = { version = "^0.28", path = "../service_framework" } +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } +tari_storage = { version = "^0.28", path = "../../infrastructure/storage" } tari_utilities = "^0.3" anyhow = "1.0.53" @@ -43,7 +43,7 @@ rustls = "0.20.2" webpki = "0.21" [dev-dependencies] -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } clap = "2.33.0" lazy_static = "1.3.0" @@ -55,7 +55,7 @@ features = ["console_appender", "file_appender", "file", "yaml_format"] default-features = false [build-dependencies] -tari_common = { version = "^0.27", path = "../../common", features = ["build"] } +tari_common = { version = "^0.28", path = "../../common", features = ["build"] } [features] test-mocks = [] diff --git a/base_layer/service_framework/Cargo.toml b/base_layer/service_framework/Cargo.toml index 1380a58851..361d7ad4ae 100644 --- a/base_layer/service_framework/Cargo.toml +++ b/base_layer/service_framework/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_service_framework" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] description = "The Tari communication stack service framework" repository = "https://github.com/tari-project/tari" @@ -10,7 +10,7 @@ license = "BSD-3-Clause" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } anyhow = "1.0.53" async-trait = "0.1.50" @@ -21,7 +21,7 @@ tokio = { version = "1.14", features = ["rt"] } tower-service = { version = "0.3" } [dev-dependencies] -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } tokio = { version = "1.14", features = ["rt-multi-thread", "macros", "time"] } futures-test = { version = "0.3.3" } diff --git a/base_layer/tari_stratum_ffi/Cargo.toml b/base_layer/tari_stratum_ffi/Cargo.toml index 80b39fe5ad..b16a8603e3 100644 --- a/base_layer/tari_stratum_ffi/Cargo.toml +++ b/base_layer/tari_stratum_ffi/Cargo.toml @@ -3,11 +3,11 @@ name = "tari_stratum_ffi" authors = ["The Tari Development Community"] description = "Tari cryptocurrency miningcore C FFI bindings" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] -tari_comms = { version = "^0.27", path = "../../comms" } +tari_comms = { version = "^0.28", path = "../../comms" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } tari_common = { path = "../../common" } tari_core = { path = "../../base_layer/core", default-features = false, features = ["transactions"]} diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index 97d46b3d4b..5af0d488bc 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -3,20 +3,20 @@ name = "tari_wallet" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet library" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] tari_common = { path = "../../common" } -tari_common_types = { version = "^0.27", path = "../../base_layer/common_types" } -tari_comms = { version = "^0.27", path = "../../comms" } -tari_comms_dht = { version = "^0.27", path = "../../comms/dht" } +tari_common_types = { version = "^0.28", path = "../../base_layer/common_types" } +tari_comms = { version = "^0.28", path = "../../comms" } +tari_comms_dht = { version = "^0.28", path = "../../comms/dht" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } -tari_key_manager = { version = "^0.27", path = "../key_manager" } -tari_p2p = { version = "^0.27", path = "../p2p", features = ["auto-update"] } -tari_service_framework = { version = "^0.27", path = "../service_framework" } -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } -tari_storage = { version = "^0.27", path = "../../infrastructure/storage" } +tari_key_manager = { version = "^0.28", path = "../key_manager" } +tari_p2p = { version = "^0.28", path = "../p2p", features = ["auto-update"] } +tari_service_framework = { version = "^0.28", path = "../service_framework" } +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } +tari_storage = { version = "^0.28", path = "../../infrastructure/storage" } tari_common_sqlite = { path = "../../common_sqlite" } tari_utilities = "0.3.0" @@ -50,14 +50,14 @@ tower = "0.4" [dependencies.tari_core] path = "../../base_layer/core" -version = "^0.27" +version = "^0.28" default-features = false features = ["transactions", "mempool_proto", "base_node_proto", ] [dev-dependencies] -tari_p2p = { version = "^0.27", path = "../p2p", features = ["test-mocks"] } -tari_comms_dht = { version = "^0.27", path = "../../comms/dht", features = ["test-mocks"] } -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_p2p = { version = "^0.28", path = "../p2p", features = ["test-mocks"] } +tari_comms_dht = { version = "^0.28", path = "../../comms/dht", features = ["test-mocks"] } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } env_logger = "0.7.1" prost = "0.9.0" diff --git a/base_layer/wallet_ffi/Cargo.toml b/base_layer/wallet_ffi/Cargo.toml index 8b2e2dff53..6c42baac94 100644 --- a/base_layer/wallet_ffi/Cargo.toml +++ b/base_layer/wallet_ffi/Cargo.toml @@ -3,18 +3,18 @@ name = "tari_wallet_ffi" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet C FFI bindings" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] -tari_comms = { version = "^0.27", path = "../../comms", features = ["c_integration"]} -tari_comms_dht = { version = "^0.27", path = "../../comms/dht", default-features = false } +tari_comms = { version = "^0.28", path = "../../comms", features = ["c_integration"]} +tari_comms_dht = { version = "^0.28", path = "../../comms/dht", default-features = false } tari_common_types = {path="../common_types"} tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } -tari_key_manager = { version = "^0.27", path = "../key_manager" } -tari_p2p = { version = "^0.27", path = "../p2p" } -tari_wallet = { version = "^0.27", path = "../wallet", features = ["c_integration"]} -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } +tari_key_manager = { version = "^0.28", path = "../key_manager" } +tari_p2p = { version = "^0.28", path = "../p2p" } +tari_wallet = { version = "^0.28", path = "../wallet", features = ["c_integration"]} +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } tari_utilities = "^0.3" chrono = { version = "0.4.19", default-features = false, features = ["serde"] } @@ -39,7 +39,7 @@ security-framework = "2.4.2" [dependencies.tari_core] path = "../../base_layer/core" -version = "^0.27" +version = "^0.28" default-features = false features = ["transactions"] @@ -49,7 +49,7 @@ crate-type = ["staticlib","cdylib"] [dev-dependencies] tempfile = "3.1.0" lazy_static = "1.3.0" -tari_key_manager = { version = "^0.27", path = "../key_manager" } -tari_common_types = { version = "^0.27", path = "../../base_layer/common_types"} -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils"} +tari_key_manager = { version = "^0.28", path = "../key_manager" } +tari_common_types = { version = "^0.28", path = "../../base_layer/common_types"} +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils"} tari_service_framework = { path = "../../base_layer/service_framework" } diff --git a/changelog.md b/changelog.md index 5c2fbd5f24..17a59c8846 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,43 @@ # Changelog +## [0.28.0](https://github.com/tari-project/tari/compare/v0.27.2...v0.28.0) (2022-02-10) + + +### ⚠ BREAKING CHANGES + +* add scanned transaction handling for one-sided payments with callbacks (#3794) +* **wallet_ffi:** add base node connectivity callback to wallet ffi (#3796) + +### Features + +* ability to compile on stable rust ([#3759](https://github.com/tari-project/tari/issues/3759)) ([c19db92](https://github.com/tari-project/tari/commit/c19db9257d2f98b2d1a456816f6ef50018bdcbfe)) +* add logging and config to collectibles ([#3781](https://github.com/tari-project/tari/issues/3781)) ([96a1e4e](https://github.com/tari-project/tari/commit/96a1e4ec144dc17190f396f94ec25c62fb142ce3)) +* add scanned transaction handling for one-sided payments with callbacks ([#3794](https://github.com/tari-project/tari/issues/3794)) ([5453c9e](https://github.com/tari-project/tari/commit/5453c9e05b7d35b7586ff9375ba30ed7ecc7a9dd)) +* add specific LibWallet error code for “Fee is greater than amount” ([#3793](https://github.com/tari-project/tari/issues/3793)) ([5aa2a66](https://github.com/tari-project/tari/commit/5aa2a661cdae869a877dda5f3cadc3abb97c374a)) +* **base-node:** add base node prometheus metrics ([#3773](https://github.com/tari-project/tari/issues/3773)) ([7502c02](https://github.com/tari-project/tari/commit/7502c020eb5f531c4ebe1a50235ab8493c8f5fd5)) +* **base-node:** add number of active sync peers metric ([#3784](https://github.com/tari-project/tari/issues/3784)) ([3495e85](https://github.com/tari-project/tari/commit/3495e85707f3ffba622feeab42e17276181654c2)) +* **collectibles:** add delete committee member button ([#3786](https://github.com/tari-project/tari/issues/3786)) ([51f2f91](https://github.com/tari-project/tari/commit/51f2f91e9b2e6289b74cf9148b23335cccea5c40)) +* prevent ambiguous output features in transaction protocols ([#3765](https://github.com/tari-project/tari/issues/3765)) ([f5b6ab6](https://github.com/tari-project/tari/commit/f5b6ab629f78497faef62f10b805d1c9a7c242c3)) +* re-use scanned range proofs ([#3764](https://github.com/tari-project/tari/issues/3764)) ([ffd502d](https://github.com/tari-project/tari/commit/ffd502d61a709d41723e67c8ec6b2d5004a87edc)) +* read asset definitions from base layer ([#3802](https://github.com/tari-project/tari/issues/3802)) ([86de08b](https://github.com/tari-project/tari/commit/86de08baa5e7648f68efcbec150d7b8652437ca9)) +* **validator_node:** add get_sidechain_block p2p rpc method ([#3803](https://github.com/tari-project/tari/issues/3803)) ([74df1d0](https://github.com/tari-project/tari/commit/74df1d0705d7acad452564e71d6fea79fc7a8daa)) +* **wallet_ffi:** add base node connectivity callback to wallet ffi ([#3796](https://github.com/tari-project/tari/issues/3796)) ([66ea697](https://github.com/tari-project/tari/commit/66ea697395286ca89b34c77f3d857f1c3f16b421)) + + +### Bug Fixes + +* bump flood ban messages config ([#3799](https://github.com/tari-project/tari/issues/3799)) ([bbd0e1e](https://github.com/tari-project/tari/commit/bbd0e1e54e3eded861b004fd2d4aeba41bc6e423)) +* coinbase output recovery bug ([#3789](https://github.com/tari-project/tari/issues/3789)) ([beb299e](https://github.com/tari-project/tari/commit/beb299e69ee1af7ec4e46889191051ce49dd1d50)) +* **comms:** minor edge-case fix to handle inbound connection while dialing ([#3785](https://github.com/tari-project/tari/issues/3785)) ([2f9603b](https://github.com/tari-project/tari/commit/2f9603b88a8db0064f1783df0b8f18be19a24497)) +* **core:** fetch_header_containing_*_mmr functions now take a 0-based mmr position ([#3749](https://github.com/tari-project/tari/issues/3749)) ([f5b72d9](https://github.com/tari-project/tari/commit/f5b72d9dd302eed0b0da612734b128b3078318ae)) +* **core:** fix potential panic for sidechain merkle root with incorrect length ([#3788](https://github.com/tari-project/tari/issues/3788)) ([b3cc6f2](https://github.com/tari-project/tari/commit/b3cc6f27359ad33fc1c3fdf49d00478f8e27994f)) +* **core:** reduce one block behind waiting period ([#3798](https://github.com/tari-project/tari/issues/3798)) ([cc41f36](https://github.com/tari-project/tari/commit/cc41f36b01a42a6f8d48b02d0ed6fe73c99f061d)) +* **ffi:** missing param in header.h ([#3774](https://github.com/tari-project/tari/issues/3774)) ([7645a83](https://github.com/tari-project/tari/commit/7645a832e4c90319ad41e74db93c1ee61daa7b2a)) +* **ffi:** mut pointers should be const ([#3775](https://github.com/tari-project/tari/issues/3775)) ([d09ba30](https://github.com/tari-project/tari/commit/d09ba304b07d9681d84f71addbe1c9c70e3c4c67)) +* fix rustls and trust-dns-client after version bump ([#3816](https://github.com/tari-project/tari/issues/3816)) ([e6e845c](https://github.com/tari-project/tari/commit/e6e845ceb219842021f5a0b359c00079a7b7eb70)) +* improved image handling in collectibles ([#3808](https://github.com/tari-project/tari/issues/3808)) ([4b22252](https://github.com/tari-project/tari/commit/4b2225291eb61955b5ff575f134d68aa47deedfe)) +* minor fixes on collectibles ([#3795](https://github.com/tari-project/tari/issues/3795)) ([cfc42dd](https://github.com/tari-project/tari/commit/cfc42ddcc5d6fd96d05922662eea43929b46c81a)) +* text explorer show sha-3 correctly + minor fixes ([#3779](https://github.com/tari-project/tari/issues/3779)) ([a5dacf2](https://github.com/tari-project/tari/commit/a5dacf2bcc51ae754d88f9af66cd0632a49b8a1b)) ### [0.27.2](https://github.com/tari-project/tari/compare/v0.27.1...v0.27.2) (2022-01-28) diff --git a/common/Cargo.toml b/common/Cargo.toml index b8cdb837e1..f2a12317c8 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [features] @@ -25,7 +25,7 @@ log4rs = { version = "1.0.0", default_features = false, features = ["config_pars multiaddr = { version = "0.13.0" } sha2 = "0.9.5" path-clean = "0.1.0" -tari_storage = { version = "^0.27", path = "../infrastructure/storage"} +tari_storage = { version = "^0.28", path = "../infrastructure/storage"} anyhow = { version = "1.0.53", optional = true } git2 = { version = "0.8", optional = true } @@ -36,5 +36,5 @@ fs2 = "0.4.3" tempfile = "3.1.0" [dev-dependencies] -tari_test_utils = { version = "^0.27", path = "../infrastructure/test_utils"} +tari_test_utils = { version = "^0.28", path = "../infrastructure/test_utils"} anyhow = "1.0.53" diff --git a/common_sqlite/Cargo.toml b/common_sqlite/Cargo.toml index 5463081ab8..0877e5ea31 100644 --- a/common_sqlite/Cargo.toml +++ b/common_sqlite/Cargo.toml @@ -3,7 +3,7 @@ name = "tari_common_sqlite" authors = ["The Tari Development Community"] description = "Tari cryptocurrency wallet library" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/comms/Cargo.toml b/comms/Cargo.toml index 8210503733..ff3263137d 100644 --- a/comms/Cargo.toml +++ b/comms/Cargo.toml @@ -6,13 +6,13 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } -tari_storage = { version = "^0.27", path = "../infrastructure/storage" } -tari_shutdown = { version = "^0.27", path = "../infrastructure/shutdown" } +tari_storage = { version = "^0.28", path = "../infrastructure/storage" } +tari_shutdown = { version = "^0.28", path = "../infrastructure/shutdown" } anyhow = "1.0.53" async-trait = "0.1.36" @@ -52,7 +52,7 @@ yamux = "=0.9.0" tari_metrics = { path = "../infrastructure/metrics" } [dev-dependencies] -tari_test_utils = { version = "^0.27", path = "../infrastructure/test_utils" } +tari_test_utils = { version = "^0.28", path = "../infrastructure/test_utils" } tari_comms_rpc_macros = { version = "*", path = "./rpc_macros" } env_logger = "0.7.0" @@ -60,7 +60,7 @@ serde_json = "1.0.39" tempfile = "3.1.0" [build-dependencies] -tari_common = { version = "^0.27", path = "../common", features = ["build"] } +tari_common = { version = "^0.28", path = "../common", features = ["build"] } [features] c_integration = [] diff --git a/comms/dht/Cargo.toml b/comms/dht/Cargo.toml index 1b0ed31c25..8fc7685d55 100644 --- a/comms/dht/Cargo.toml +++ b/comms/dht/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tari_comms_dht" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] description = "Tari comms DHT module" repository = "https://github.com/tari-project/tari" @@ -10,12 +10,12 @@ license = "BSD-3-Clause" edition = "2018" [dependencies] -tari_comms = { version = "^0.27", path = "../", features = ["rpc"] } -tari_comms_rpc_macros = { version = "^0.27", path = "../rpc_macros" } +tari_comms = { version = "^0.28", path = "../", features = ["rpc"] } +tari_comms_rpc_macros = { version = "^0.28", path = "../rpc_macros" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } tari_utilities = { version = "^0.3" } -tari_shutdown = { version = "^0.27", path = "../../infrastructure/shutdown" } -tari_storage = { version = "^0.27", path = "../../infrastructure/storage" } +tari_shutdown = { version = "^0.28", path = "../../infrastructure/shutdown" } +tari_storage = { version = "^0.28", path = "../../infrastructure/storage" } tari_common_sqlite = { path = "../../common_sqlite" } anyhow = "1.0.53" @@ -43,7 +43,7 @@ tower = { version = "0.4", features = ["full"] } pin-project = "0.4" [dev-dependencies] -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } env_logger = "0.7.0" futures-test = { version = "0.3.5" } @@ -56,7 +56,7 @@ petgraph = "0.5.1" clap = "2.33.0" [build-dependencies] -tari_common = { version = "^0.27", path = "../../common" } +tari_common = { version = "^0.28", path = "../../common" } [features] test-mocks = [] diff --git a/comms/rpc_macros/Cargo.toml b/comms/rpc_macros/Cargo.toml index 3f14ba0927..ac8c0f2719 100644 --- a/comms/rpc_macros/Cargo.toml +++ b/comms/rpc_macros/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [lib] @@ -19,8 +19,8 @@ quote = "1.0.7" syn = { version = "1.0.38", features = ["fold"] } [dev-dependencies] -tari_comms = { version = "^0.27", path = "../", features = ["rpc"] } -tari_test_utils = { version = "^0.27", path = "../../infrastructure/test_utils" } +tari_comms = { version = "^0.28", path = "../", features = ["rpc"] } +tari_test_utils = { version = "^0.28", path = "../../infrastructure/test_utils" } futures = "0.3.5" prost = "0.9.0" diff --git a/infrastructure/derive/Cargo.toml b/infrastructure/derive/Cargo.toml index 8f064c558e..ee6afa6303 100644 --- a/infrastructure/derive/Cargo.toml +++ b/infrastructure/derive/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [lib] diff --git a/infrastructure/libtor/Cargo.toml b/infrastructure/libtor/Cargo.toml index 8e71a142c1..31f9a53cb7 100644 --- a/infrastructure/libtor/Cargo.toml +++ b/infrastructure/libtor/Cargo.toml @@ -11,7 +11,7 @@ multiaddr = { version = "0.13.0" } # NB: make sure this crate is not included in any other crate used by wallet_ffi [target.'cfg(unix)'.dependencies] -tari_shutdown = { version = "^0.27", path = "../shutdown"} +tari_shutdown = { version = "^0.28", path = "../shutdown"} libtor = { version = "46.9.0", optional = true } rand = "0.8" tempfile = "3.1.0" diff --git a/infrastructure/shutdown/Cargo.toml b/infrastructure/shutdown/Cargo.toml index b07254e6dd..9d959c79b3 100644 --- a/infrastructure/shutdown/Cargo.toml +++ b/infrastructure/shutdown/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/infrastructure/storage/Cargo.toml b/infrastructure/storage/Cargo.toml index 86b8b4e071..ba54e3a5ab 100644 --- a/infrastructure/storage/Cargo.toml +++ b/infrastructure/storage/Cargo.toml @@ -6,7 +6,7 @@ repository = "https://github.com/tari-project/tari" homepage = "https://tari.com" readme = "README.md" license = "BSD-3-Clause" -version = "0.27.3" +version = "0.28.0" edition = "2018" [dependencies] diff --git a/infrastructure/test_utils/Cargo.toml b/infrastructure/test_utils/Cargo.toml index e0aa464c61..b135eb0172 100644 --- a/infrastructure/test_utils/Cargo.toml +++ b/infrastructure/test_utils/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tari_test_utils" description = "Utility functions used in Tari test functions" -version = "0.27.3" +version = "0.28.0" authors = ["The Tari Development Community"] edition = "2018" license = "BSD-3-Clause" diff --git a/package-lock.json b/package-lock.json index d3287a6b0b..befbdd2466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tari", - "version": "0.27.2", + "version": "0.28.0", "lockfileVersion": 2, "requires": true, "packages": {} diff --git a/package.json b/package.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/package.json +++ /dev/null @@ -1 +0,0 @@ -{} From e0172e5108b7bfd00a852136c45eab91628c1d31 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 10 Feb 2022 19:23:22 +0200 Subject: [PATCH 06/28] chore(p2p): remove unused config from liveness service (#3818) Description --- Removes unused config for liveness service Motivation and Context --- Minor cleanup How Has This Been Tested? --- Code compiles --- applications/tari_base_node/src/bootstrap.rs | 1 - base_layer/p2p/src/services/liveness/config.rs | 6 ------ 2 files changed, 7 deletions(-) diff --git a/applications/tari_base_node/src/bootstrap.rs b/applications/tari_base_node/src/bootstrap.rs index e86fbe0d7d..d10eba63fa 100644 --- a/applications/tari_base_node/src/bootstrap.rs +++ b/applications/tari_base_node/src/bootstrap.rs @@ -147,7 +147,6 @@ where B: BlockchainBackend + 'static .add_initializer(LivenessInitializer::new( LivenessConfig { auto_ping_interval: Some(Duration::from_secs(config.auto_ping_interval)), - refresh_neighbours_interval: Duration::from_secs(3 * 60), monitored_peers: sync_peers.clone(), ..Default::default() }, diff --git a/base_layer/p2p/src/services/liveness/config.rs b/base_layer/p2p/src/services/liveness/config.rs index 3a53a8f354..87d6dda341 100644 --- a/base_layer/p2p/src/services/liveness/config.rs +++ b/base_layer/p2p/src/services/liveness/config.rs @@ -29,10 +29,6 @@ use tari_comms::peer_manager::NodeId; pub struct LivenessConfig { /// The interval to send Ping messages, or None to disable periodic pinging (default: None (disabled)) pub auto_ping_interval: Option, - /// The length of time between querying peer manager for closest neighbours. (default: 2 minutes) - pub refresh_neighbours_interval: Duration, - /// The length of time between querying peer manager for random neighbours. (default: 2 hours) - pub refresh_random_pool_interval: Duration, /// Number of peers to ping per round, excluding monitored peers (Default: 8) pub num_peers_per_round: usize, /// Peers to include in every auto ping round (Default: ) @@ -45,8 +41,6 @@ impl Default for LivenessConfig { fn default() -> Self { Self { auto_ping_interval: None, - refresh_neighbours_interval: Duration::from_secs(2 * 60), - refresh_random_pool_interval: Duration::from_secs(2 * 60 * 60), num_peers_per_round: 8, monitored_peers: Default::default(), max_allowed_ping_failures: 2, From 77233d42c8de2c653529460ab40c4fd3e6fbbda1 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Fri, 11 Feb 2022 10:27:12 +0200 Subject: [PATCH 07/28] test: fix transaction output hash calculation in cucumber (#3819) Description --- - Fixed the transaction output hash calculation according to the recent consensus encoding rules. - Removed `@broken` tags from 2x critical tests. Motivation and Context --- See above. How Has This Been Tested? --- Cucumber tests: - `As a wallet I want to submit a transaction` - `As a wallet I cannot submit a locked coinbase transaction` --- .../features/WalletQuery.feature | 4 +- integration_tests/helpers/util.js | 91 +++++++++++++++++-- integration_tests/package-lock.json | 56 ++++++------ 3 files changed, 111 insertions(+), 40 deletions(-) diff --git a/integration_tests/features/WalletQuery.feature b/integration_tests/features/WalletQuery.feature index 71f812be96..a0e56a0d5c 100644 --- a/integration_tests/features/WalletQuery.feature +++ b/integration_tests/features/WalletQuery.feature @@ -8,7 +8,7 @@ Feature: Wallet Querying Then node WalletSeedA is at height 1 Then the UTXO CB1 has been mined according to WalletSeedA - @critical @broken + @critical Scenario: As a wallet I want to submit a transaction Given I have a seed node SeedA When I mine a block on SeedA with coinbase CB1 @@ -20,7 +20,7 @@ Feature: Wallet Querying Then the UTXO UTX1 has been mined according to SeedA - @critical @broken + @critical Scenario: As a wallet I cannot submit a locked coinbase transaction # Using GRPC Given I have a seed node SeedA diff --git a/integration_tests/helpers/util.js b/integration_tests/helpers/util.js index 7b6d808b68..411dd8072f 100644 --- a/integration_tests/helpers/util.js +++ b/integration_tests/helpers/util.js @@ -1,6 +1,7 @@ const net = require("net"); const { blake2bInit, blake2bUpdate, blake2bFinal } = require("blakejs"); +const { expect } = require("chai"); const NO_CONNECTION = 14; @@ -200,24 +201,94 @@ const getFreePort = function () { }); }; +const encodeOption = function (value) { + let buffer; + if (value) { + buffer = Buffer.concat([Buffer.from([1]), Buffer.from(value, "utf8")]); + } else { + buffer = Buffer.from([0]); + } + return buffer; +}; + const getTransactionOutputHash = function (output) { const KEY = null; // optional key const OUTPUT_LENGTH = 32; // bytes const context = blake2bInit(OUTPUT_LENGTH, KEY); - const flags = Buffer.alloc(1); - flags[0] = output.features.flags; - const buffer = Buffer.concat([ - Buffer.from([0]), // base_layer\core\src\transactions\transaction\output_features.rs:64 CONSENSUS_ENCODING_VERSION : u8 = 0 + let encodedBytesLength = 0; + // version + const version = Buffer.from([0]); + encodedBytesLength += version.length; + blake2bUpdate(context, version); + // features + let features = Buffer.concat([ + // features.version + Buffer.from([0]), + // features.maturity Buffer.from([parseInt(output.features.maturity)]), + // features.flags Buffer.from([output.features.flags]), ]); - let nopScriptBytes = Buffer.from([0x73]); - - blake2bUpdate(context, buffer); + // features.parent_public_key + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.parent_public_key), + ]); + // features.unique_id + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.unique_id), + ]); + // features.asset + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.asset), + ]); + // features.mint_non_fungible + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.mint_non_fungible), + ]); + // features.sidechain_checkpoint + features = Buffer.concat([ + Buffer.from(features), + encodeOption(output.features.sidechain_checkpoint), + ]); + // features.metadata + features = Buffer.concat([ + Buffer.from(features), + Buffer.from([output.features.metadata.length]), + Buffer.from(output.features.metadata), + ]); + encodedBytesLength += features.length; + blake2bUpdate(context, features); + // commitment + encodedBytesLength += output.commitment.length; blake2bUpdate(context, output.commitment); - blake2bUpdate(context, nopScriptBytes); - let final = blake2bFinal(context); - return Buffer.from(final); + // script + const script = Buffer.concat([ + Buffer.from([output.script.length]), + Buffer.from(output.script), + ]); + encodedBytesLength += script.length; + blake2bUpdate(context, script); + // covenant + const covenant = Buffer.concat([ + Buffer.from([output.covenant.length]), + Buffer.from(output.covenant), + ]); + encodedBytesLength += covenant.length; + blake2bUpdate(context, covenant); + + expect(context.c).to.equal(encodedBytesLength); + const hash = blake2bFinal(context); + const hashBuffer = Buffer.from(hash); + // console.log( + // "\ngetTransactionOutputHash - hash", + // hashBuffer.toString("hex"), + // "\n" + // ); + return hashBuffer; }; function consoleLogTransactionDetails(txnDetails) { diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index 63d7981e34..b0a9b0ba99 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -653,7 +653,7 @@ }, "any-promise": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "resolved": false, "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", "dev": true }, @@ -747,7 +747,7 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "resolved": false, "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, @@ -759,7 +759,7 @@ }, "assertion-error-formatter": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", + "resolved": false, "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", "dev": true, "requires": { @@ -993,7 +993,7 @@ }, "colors": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "resolved": false, "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" }, "commander": { @@ -1088,7 +1088,7 @@ }, "d": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "resolved": false, "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, "requires": { @@ -1152,7 +1152,7 @@ }, "diff": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "resolved": false, "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, @@ -1167,7 +1167,7 @@ }, "duration": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz", + "resolved": false, "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", "dev": true, "requires": { @@ -1259,7 +1259,7 @@ }, "es5-ext": { "version": "0.10.53", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "resolved": false, "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { @@ -1270,7 +1270,7 @@ }, "es6-iterator": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "resolved": false, "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { @@ -1281,7 +1281,7 @@ }, "es6-symbol": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "resolved": false, "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { @@ -1704,7 +1704,7 @@ }, "ext": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.6.0.tgz", + "resolved": false, "integrity": "sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==", "dev": true, "requires": { @@ -1713,7 +1713,7 @@ "dependencies": { "type": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "resolved": false, "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } @@ -1765,7 +1765,7 @@ }, "figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "resolved": false, "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "requires": { "escape-string-regexp": "^1.0.5" @@ -2042,7 +2042,7 @@ }, "indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "resolved": false, "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, @@ -2175,7 +2175,7 @@ }, "is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "resolved": false, "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, @@ -2328,7 +2328,7 @@ }, "knuth-shuffle-seeded": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", + "resolved": false, "integrity": "sha1-AfG2VzOqdUDuCNiwF0Fk0iCB5OE=", "dev": true, "requires": { @@ -2504,7 +2504,7 @@ }, "mz": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "resolved": false, "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, "requires": { @@ -2521,7 +2521,7 @@ }, "next-tick": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "resolved": false, "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, @@ -2574,7 +2574,7 @@ }, "object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { @@ -2669,7 +2669,7 @@ }, "pad-right": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "resolved": false, "integrity": "sha1-b7ySQEXSRPKiokRQMGDTv8YAl3Q=", "dev": true, "requires": { @@ -2877,7 +2877,7 @@ }, "repeat-string": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "resolved": false, "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, @@ -2942,7 +2942,7 @@ }, "seed-random": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "resolved": false, "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=", "dev": true }, @@ -3068,7 +3068,7 @@ }, "stack-chain": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", + "resolved": false, "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", "dev": true }, @@ -3118,7 +3118,7 @@ }, "string-argv": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "resolved": false, "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", "dev": true }, @@ -3263,7 +3263,7 @@ }, "thenify": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "resolved": false, "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { @@ -3272,7 +3272,7 @@ }, "thenify-all": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "resolved": false, "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", "dev": true, "requires": { @@ -3336,7 +3336,7 @@ }, "type": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "resolved": false, "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, @@ -3398,7 +3398,7 @@ }, "util-arity": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", + "resolved": false, "integrity": "sha1-WdAa8f2z/t4KxOYysKtfbOl8kzA=", "dev": true }, From b1492a0b60215640513a3b676b056e022ea64226 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Fri, 11 Feb 2022 16:21:44 +0200 Subject: [PATCH 08/28] chore: review wallet based TODOs (#3825) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description --- This PR is part of a review of the TODO statements in the code related to the wallet. In this PR I have gone through the TODOs related to the wallet and I have dealt with them in one of three ways 1. Deleted them if I don’t think they are relevant anymore. In this case I will call them out in the PR to be reviewed specifically with a PR comment 2. Fixed them if it felt like a quick fix, I will also call out this fix in a PR comment 3. Logged them in an issue for future attention. In this case I tagged them with the #LOGGED tag --- .../src/conversions/output_features.rs | 2 +- .../src/conversions/transaction.rs | 2 +- .../src/conversions/transaction_input.rs | 2 +- .../src/conversions/transaction_kernel.rs | 2 +- .../src/conversions/transaction_output.rs | 2 +- .../src/conversions/unblinded_output.rs | 2 +- .../src/grpc/base_node_grpc_server.rs | 2 +- .../src/automation/command_parser.rs | 4 +- .../src/automation/commands.rs | 8 ++-- .../src/automation/error.rs | 2 +- .../src/grpc/wallet_grpc_server.rs | 6 +-- .../tari_console_wallet/src/init/mod.rs | 2 +- applications/tari_console_wallet/src/main.rs | 12 +++-- .../src/ui/components/notification_tab.rs | 4 +- .../src/ui/components/send_tab.rs | 4 +- .../src/ui/components/tabs_container.rs | 1 - .../src/ui/components/transactions_tab.rs | 8 +++- .../src/ui/state/app_state.rs | 7 +-- .../src/common/merge_mining.rs | 2 +- .../src/common/proxy.rs | 1 - .../src/common/mining.rs | 2 +- .../src/common/proxy.rs | 1 - applications/test_faucet/src/main.rs | 2 +- .../comms_interface/comms_response.rs | 2 +- .../comms_interface/local_interface.rs | 2 +- base_layer/core/src/base_node/rpc/service.rs | 3 +- .../states/horizon_state_sync/error.rs | 2 +- .../horizon_state_synchronization.rs | 2 +- base_layer/core/src/blocks/block.rs | 2 +- base_layer/core/src/blocks/genesis_block.rs | 2 +- base_layer/core/src/chain_storage/async_db.rs | 2 +- .../src/chain_storage/blockchain_backend.rs | 2 +- .../src/chain_storage/blockchain_database.rs | 2 +- .../core/src/chain_storage/db_transaction.rs | 2 +- base_layer/core/src/chain_storage/error.rs | 2 +- .../core/src/chain_storage/lmdb_db/lmdb_db.rs | 2 +- .../core/src/chain_storage/lmdb_db/mod.rs | 2 +- .../core/src/chain_storage/pruned_output.rs | 2 +- .../tests/blockchain_database.rs | 4 +- .../core/src/consensus/consensus_constants.rs | 2 +- .../core/src/consensus/consensus_manager.rs | 2 +- base_layer/core/src/covenants/context.rs | 2 +- base_layer/core/src/covenants/covenant.rs | 2 +- base_layer/core/src/covenants/fields.rs | 4 +- .../src/covenants/filters/fields_hashed_eq.rs | 2 +- .../src/covenants/filters/fields_preserved.rs | 2 +- base_layer/core/src/covenants/filters/test.rs | 2 +- base_layer/core/src/covenants/output_set.rs | 2 +- base_layer/core/src/covenants/test.rs | 2 +- base_layer/core/src/mempool/error.rs | 2 +- base_layer/core/src/mempool/mempool.rs | 2 +- .../core/src/mempool/mempool_storage.rs | 2 +- base_layer/core/src/mempool/mod.rs | 2 +- .../priority/prioritized_transaction.rs | 2 +- .../core/src/mempool/reorg_pool/reorg_pool.rs | 2 +- base_layer/core/src/mempool/rpc/service.rs | 2 +- base_layer/core/src/mempool/service/handle.rs | 2 +- .../src/mempool/service/inbound_handlers.rs | 2 +- .../core/src/mempool/service/initializer.rs | 2 +- .../core/src/mempool/service/local_service.rs | 2 +- .../src/mempool/service/outbound_interface.rs | 2 +- .../core/src/mempool/service/request.rs | 2 +- .../core/src/mempool/service/service.rs | 2 +- .../core/src/mempool/sync_protocol/mod.rs | 2 +- .../core/src/mempool/sync_protocol/test.rs | 2 +- .../unconfirmed_pool/unconfirmed_pool.rs | 4 +- base_layer/core/src/proto/transaction.rs | 2 +- .../core/src/test_helpers/block_spec.rs | 2 +- .../core/src/test_helpers/blockchain.rs | 2 +- base_layer/core/src/test_helpers/mod.rs | 2 +- .../core/src/transactions/aggregated_body.rs | 2 +- .../core/src/transactions/coinbase_builder.rs | 5 +- base_layer/core/src/transactions/mod.rs | 2 +- .../core/src/transactions/tari_amount.rs | 6 +-- .../core/src/transactions/test_helpers.rs | 4 +- .../asset_output_features.rs | 2 +- .../error.rs | 0 .../full_rewind_result.rs | 0 .../kernel_builder.rs | 2 +- .../kernel_features.rs | 0 .../kernel_sum.rs | 0 .../mint_non_fungible_features.rs | 0 .../mod.rs | 2 - .../output_features.rs | 2 +- .../output_features_version.rs | 0 .../output_flags.rs | 0 .../rewind_result.rs | 0 .../side_chain_checkpoint_features.rs | 0 .../template_parameter.rs | 0 .../test.rs | 2 +- .../transaction.rs | 8 +++- .../transaction_builder.rs | 2 +- .../transaction_input.rs | 11 +++-- .../transaction_input_version.rs | 0 .../transaction_kernel.rs | 2 +- .../transaction_kernel_version.rs | 0 .../transaction_output.rs | 6 +-- .../transaction_output_version.rs | 0 .../unblinded_output.rs | 8 ++-- .../unblinded_output_builder.rs | 2 +- .../transactions/transaction_protocol/mod.rs | 2 +- .../proto/transaction_sender.proto | 3 -- .../transaction_protocol/recipient.rs | 4 +- .../transaction_protocol/sender.rs | 7 ++- .../transaction_protocol/single_receiver.rs | 4 +- .../transaction_initializer.rs | 9 ++-- .../block_validators/async_validator.rs | 2 +- .../src/validation/block_validators/test.rs | 2 +- base_layer/core/src/validation/error.rs | 2 +- base_layer/core/src/validation/helpers.rs | 4 +- base_layer/core/src/validation/mocks.rs | 2 +- base_layer/core/src/validation/test.rs | 2 +- base_layer/core/src/validation/traits.rs | 2 +- .../src/validation/transaction_validators.rs | 2 +- base_layer/core/tests/async_db.rs | 2 +- base_layer/core/tests/base_node_rpc.rs | 2 +- base_layer/core/tests/block_validation.rs | 2 +- .../chain_storage_tests/chain_storage.rs | 2 +- .../core/tests/helpers/block_builders.rs | 2 +- base_layer/core/tests/helpers/database.rs | 2 +- .../core/tests/helpers/sample_blockchains.rs | 2 +- .../core/tests/helpers/test_block_builder.rs | 2 +- .../core/tests/helpers/test_blockchain.rs | 2 +- base_layer/core/tests/mempool.rs | 2 +- base_layer/core/tests/node_comms_interface.rs | 2 +- base_layer/core/tests/node_service.rs | 2 +- base_layer/key_manager/src/mnemonic.rs | 1 + base_layer/wallet/src/assets/asset_manager.rs | 2 +- .../wallet/src/assets/asset_manager_handle.rs | 2 +- .../wallet/src/assets/infrastructure/mod.rs | 2 +- .../mock_base_node_service.rs | 2 +- base_layer/wallet/src/error.rs | 3 +- .../src/output_manager_service/error.rs | 2 +- .../src/output_manager_service/handle.rs | 8 +++- .../recovery/standard_outputs_recoverer.rs | 29 ++++++------ .../src/output_manager_service/service.rs | 7 ++- .../storage/database/backend.rs | 2 +- .../storage/database/mod.rs | 2 +- .../output_manager_service/storage/models.rs | 6 ++- .../storage/sqlite_db/mod.rs | 46 +++++++++++++------ .../storage/sqlite_db/output_sql.rs | 3 +- .../tasks/txo_validation_task.rs | 2 - .../src/storage/sqlite_utilities/mod.rs | 24 ---------- base_layer/wallet/src/tokens/token_manager.rs | 2 +- .../wallet/src/transaction_service/error.rs | 7 ++- .../wallet/src/transaction_service/handle.rs | 3 +- .../transaction_broadcast_protocol.rs | 2 +- .../protocols/transaction_receive_protocol.rs | 2 +- .../protocols/transaction_send_protocol.rs | 3 +- .../wallet/src/transaction_service/service.rs | 18 +++----- .../transaction_service/storage/database.rs | 3 +- .../src/transaction_service/storage/models.rs | 2 +- .../transaction_service/storage/sqlite_db.rs | 4 +- .../tasks/send_finalized_transaction.rs | 2 +- .../src/utxo_scanner_service/service.rs | 2 +- .../utxo_scanner_service/utxo_scanner_task.rs | 2 +- base_layer/wallet/src/wallet.rs | 2 +- .../output_manager_service_tests/service.rs | 2 +- base_layer/wallet/tests/support/comms_rpc.rs | 7 +-- base_layer/wallet/tests/support/utils.rs | 2 +- .../transaction_service_tests/service.rs | 2 +- .../transaction_service_tests/storage.rs | 2 +- base_layer/wallet/tests/utxo_scanner.rs | 2 +- base_layer/wallet/tests/wallet.rs | 2 +- .../wallet_ffi/src/callback_handler_tests.rs | 2 +- base_layer/wallet_ffi/src/lib.rs | 8 ++-- base_layer/wallet_ffi/wallet.h | 1 - comms/dht/src/dedup/dedup_cache.rs | 3 +- comms/dht/src/proto/envelope.proto | 2 +- dan_layer/core/src/models/asset_definition.rs | 2 +- .../core/src/models/base_layer_output.rs | 2 +- .../core/src/services/asset_processor.rs | 2 +- dan_layer/core/src/services/mocks/mod.rs | 2 +- .../core/src/services/payload_processor.rs | 2 +- .../core/src/templates/tip002_template.rs | 2 +- 175 files changed, 279 insertions(+), 285 deletions(-) rename base_layer/core/src/transactions/{transaction => transaction_components}/asset_output_features.rs (98%) rename base_layer/core/src/transactions/{transaction => transaction_components}/error.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/full_rewind_result.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/kernel_builder.rs (97%) rename base_layer/core/src/transactions/{transaction => transaction_components}/kernel_features.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/kernel_sum.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/mint_non_fungible_features.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/mod.rs (98%) rename base_layer/core/src/transactions/{transaction => transaction_components}/output_features.rs (99%) rename base_layer/core/src/transactions/{transaction => transaction_components}/output_features_version.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/output_flags.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/rewind_result.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/side_chain_checkpoint_features.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/template_parameter.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/test.rs (99%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction.rs (97%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_builder.rs (97%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_input.rs (98%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_input_version.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_kernel.rs (98%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_kernel_version.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_output.rs (99%) rename base_layer/core/src/transactions/{transaction => transaction_components}/transaction_output_version.rs (100%) rename base_layer/core/src/transactions/{transaction => transaction_components}/unblinded_output.rs (98%) rename base_layer/core/src/transactions/{transaction => transaction_components}/unblinded_output_builder.rs (98%) diff --git a/applications/tari_app_grpc/src/conversions/output_features.rs b/applications/tari_app_grpc/src/conversions/output_features.rs index 969f62374d..7c0a009248 100644 --- a/applications/tari_app_grpc/src/conversions/output_features.rs +++ b/applications/tari_app_grpc/src/conversions/output_features.rs @@ -26,7 +26,7 @@ use tari_common_types::{ array::copy_into_fixed_array, types::{Commitment, PublicKey}, }; -use tari_core::transactions::transaction::{ +use tari_core::transactions::transaction_components::{ AssetOutputFeatures, MintNonFungibleFeatures, OutputFeatures, diff --git a/applications/tari_app_grpc/src/conversions/transaction.rs b/applications/tari_app_grpc/src/conversions/transaction.rs index f74dc1e66f..2757e22188 100644 --- a/applications/tari_app_grpc/src/conversions/transaction.rs +++ b/applications/tari_app_grpc/src/conversions/transaction.rs @@ -26,7 +26,7 @@ use std::{ }; use tari_common_types::transaction::{TransactionDirection, TransactionStatus, TxId}; -use tari_core::transactions::transaction::Transaction; +use tari_core::transactions::transaction_components::Transaction; use tari_crypto::ristretto::RistrettoSecretKey; use tari_utilities::ByteArray; diff --git a/applications/tari_app_grpc/src/conversions/transaction_input.rs b/applications/tari_app_grpc/src/conversions/transaction_input.rs index b35fcb1391..7856784ab2 100644 --- a/applications/tari_app_grpc/src/conversions/transaction_input.rs +++ b/applications/tari_app_grpc/src/conversions/transaction_input.rs @@ -25,7 +25,7 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::{Commitment, PublicKey}; use tari_core::{ covenants::Covenant, - transactions::transaction::{TransactionInput, TransactionInputVersion}, + transactions::transaction_components::{TransactionInput, TransactionInputVersion}, }; use tari_crypto::{ script::{ExecutionStack, TariScript}, diff --git a/applications/tari_app_grpc/src/conversions/transaction_kernel.rs b/applications/tari_app_grpc/src/conversions/transaction_kernel.rs index 3d92769e4f..b7e835e22c 100644 --- a/applications/tari_app_grpc/src/conversions/transaction_kernel.rs +++ b/applications/tari_app_grpc/src/conversions/transaction_kernel.rs @@ -25,7 +25,7 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::Commitment; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, TransactionKernel, TransactionKernelVersion}, + transaction_components::{KernelFeatures, TransactionKernel, TransactionKernelVersion}, }; use tari_crypto::tari_utilities::{ByteArray, Hashable}; diff --git a/applications/tari_app_grpc/src/conversions/transaction_output.rs b/applications/tari_app_grpc/src/conversions/transaction_output.rs index 1e3b91257d..a22dcd3cf3 100644 --- a/applications/tari_app_grpc/src/conversions/transaction_output.rs +++ b/applications/tari_app_grpc/src/conversions/transaction_output.rs @@ -25,7 +25,7 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::{BulletRangeProof, Commitment, PublicKey}; use tari_core::{ covenants::Covenant, - transactions::transaction::{TransactionOutput, TransactionOutputVersion}, + transactions::transaction_components::{TransactionOutput, TransactionOutputVersion}, }; use tari_crypto::script::TariScript; use tari_utilities::{ByteArray, Hashable}; diff --git a/applications/tari_app_grpc/src/conversions/unblinded_output.rs b/applications/tari_app_grpc/src/conversions/unblinded_output.rs index 3979087040..395887c768 100644 --- a/applications/tari_app_grpc/src/conversions/unblinded_output.rs +++ b/applications/tari_app_grpc/src/conversions/unblinded_output.rs @@ -27,7 +27,7 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::{TransactionOutputVersion, UnblindedOutput}, + transaction_components::{TransactionOutputVersion, UnblindedOutput}, }, }; use tari_crypto::script::{ExecutionStack, TariScript}; diff --git a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs index 994616ce5a..57f2b5d6d0 100644 --- a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs +++ b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs @@ -48,7 +48,7 @@ use tari_core::{ iterators::NonOverlappingIntegerPairIter, mempool::{service::LocalMempoolService, TxStorageResponse}, proof_of_work::PowAlgorithm, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; use tari_p2p::{auto_update::SoftwareUpdaterHandle, services::liveness::LivenessHandle}; use tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray, Hashable}; diff --git a/applications/tari_console_wallet/src/automation/command_parser.rs b/applications/tari_console_wallet/src/automation/command_parser.rs index 3d58ae3371..d5e53ff7f6 100644 --- a/applications/tari_console_wallet/src/automation/command_parser.rs +++ b/applications/tari_console_wallet/src/automation/command_parser.rs @@ -128,8 +128,8 @@ pub fn parse_command(command: &str) -> Result { CoinSplit => parse_coin_split(args)?, DiscoverPeer => parse_public_key(args)?, Whois => parse_whois(args)?, - ExportUtxos => parse_export_utxos(args)?, // todo: only show X number of utxos - ExportSpentUtxos => parse_export_spent_utxos(args)?, // todo: only show X number of utxos + ExportUtxos => parse_export_utxos(args)?, + ExportSpentUtxos => parse_export_spent_utxos(args)?, CountUtxos => Vec::new(), SetBaseNode => parse_public_key_and_address(args)?, SetCustomBaseNode => parse_public_key_and_address(args)?, diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 7404be6eaa..f9065b5aa9 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -44,7 +44,7 @@ use tari_comms::{ use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; use tari_core::transactions::{ tari_amount::{uT, MicroTari, Tari}, - transaction::{TransactionOutput, UnblindedOutput}, + transaction_components::{TransactionOutput, UnblindedOutput}, }; use tari_crypto::{ keys::PublicKey as PublicKeyTrait, @@ -113,7 +113,7 @@ pub struct SentTransaction {} fn get_transaction_parameters( args: Vec, ) -> Result<(MicroTari, MicroTari, PublicKey, String), CommandError> { - // TODO: Consolidate "fee per gram" in codebase + // TODO: Consolidate "fee per gram" in codebase #LOGGED let fee_per_gram = 25 * uT; use ParsedArgument::*; @@ -138,7 +138,7 @@ fn get_transaction_parameters( fn get_init_sha_atomic_swap_parameters( args: Vec, ) -> Result<(MicroTari, MicroTari, PublicKey, String), CommandError> { - // TODO: Consolidate "fee per gram" in codebase + // TODO: Consolidate "fee per gram" in codebase #LOGGED let fee_per_gram = 25 * uT; use ParsedArgument::*; @@ -801,7 +801,7 @@ pub async fn command_runner( let name = parsed.args[0].to_string(); let message = format!("Register asset: {}", name); let mut manager = wallet.asset_manager.clone(); - // todo: key manager + // todo: key manager #LOGGED let mut rng = rand::thread_rng(); let (_, public_key) = PublicKey::random_keypair(&mut rng); let (tx_id, transaction) = manager diff --git a/applications/tari_console_wallet/src/automation/error.rs b/applications/tari_console_wallet/src/automation/error.rs index bc0d114361..ea521dd8e6 100644 --- a/applications/tari_console_wallet/src/automation/error.rs +++ b/applications/tari_console_wallet/src/automation/error.rs @@ -26,7 +26,7 @@ use log::*; use tari_common::exit_codes::{ExitCode, ExitError}; use tari_core::transactions::{ tari_amount::{MicroTariError, TariConversionError}, - transaction::TransactionError, + transaction_components::TransactionError, }; use tari_utilities::hex::HexError; use tari_wallet::{ diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index d394bcb338..68fe030250 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -79,7 +79,7 @@ use tari_common_types::{ use tari_comms::{types::CommsPublicKey, CommsNode}; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::{OutputFeatures, UnblindedOutput}, + transaction_components::{OutputFeatures, UnblindedOutput}, }; use tari_crypto::{ristretto::RistrettoPublicKey, tari_utilities::Hashable}; use tari_utilities::{hex::Hex, ByteArray}; @@ -739,8 +739,8 @@ impl wallet_server::Wallet for WalletGrpcServer { let mut transaction_service = self.wallet.transaction_service.clone(); let message = request.into_inner(); - // TODO: Clean up unwrap - let asset_public_key = PublicKey::from_bytes(message.asset_public_key.as_slice()).unwrap(); + let asset_public_key = + PublicKey::from_bytes(message.asset_public_key.as_slice()).map_err(|e| Status::internal(e.to_string()))?; let asset = asset_manager .get_owned_asset_by_pub_key(&asset_public_key) .await diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 2bfff7227c..7ad81f0542 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -534,7 +534,7 @@ pub async fn start_wallet( base_node: &Peer, wallet_mode: &WalletMode, ) -> Result<(), ExitError> { - // TODO gRPC interfaces for setting base node + // TODO gRPC interfaces for setting base node #LOGGED debug!(target: LOG_TARGET, "Setting base node peer"); let net_address = base_node diff --git a/applications/tari_console_wallet/src/main.rs b/applications/tari_console_wallet/src/main.rs index ccc6c6bbe6..50e166faa8 100644 --- a/applications/tari_console_wallet/src/main.rs +++ b/applications/tari_console_wallet/src/main.rs @@ -242,10 +242,14 @@ fn enable_tracing() { global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); let tracer = opentelemetry_jaeger::new_pipeline() .with_service_name("tari::console_wallet") - .with_tags(vec![KeyValue::new("pid", process::id().to_string()), KeyValue::new("current_exe", env::current_exe().unwrap().to_str().unwrap_or_default().to_owned())]) - // TODO: uncomment when using tokio 1 - // .install_batch(opentelemetry::runtime::Tokio) - .install_simple() + .with_tags(vec![ + KeyValue::new("pid", process::id().to_string()), + KeyValue::new( + "current_exe", + env::current_exe().unwrap().to_str().unwrap_or_default().to_owned(), + ), + ]) + .install_batch(opentelemetry::runtime::Tokio) .unwrap(); let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); let subscriber = Registry::default().with(telemetry); diff --git a/applications/tari_console_wallet/src/ui/components/notification_tab.rs b/applications/tari_console_wallet/src/ui/components/notification_tab.rs index 4e15ca6c14..e0866d2c8f 100644 --- a/applications/tari_console_wallet/src/ui/components/notification_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/notification_tab.rs @@ -4,9 +4,9 @@ // are cleared. // Currently notifications are only added from the wallet_event_monitor which has // add_notification method. -// TODO: auto delete old notifications. +// TODO: auto delete old notifications. #LOGGED // TODO: add interaction with the notifications, e.g. if I have a pending transaction -// notification, the UI should go there if I click on it. +// notification, the UI should go there if I click on it. #LOGGED use tari_comms::runtime::Handle; use tui::{ diff --git a/applications/tari_console_wallet/src/ui/components/send_tab.rs b/applications/tari_console_wallet/src/ui/components/send_tab.rs index 43af6be008..2595deec50 100644 --- a/applications/tari_console_wallet/src/ui/components/send_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/send_tab.rs @@ -401,7 +401,7 @@ impl SendTab { self.to_field.clone(), amount.into(), self.selected_unique_id.clone(), - None, // TODO: Select the actual value + None, fee_per_gram, self.message_field.clone(), tx, @@ -419,7 +419,7 @@ impl SendTab { self.to_field.clone(), amount.into(), self.selected_unique_id.clone(), - None, // TODO: select actual value + None, fee_per_gram, self.message_field.clone(), tx, diff --git a/applications/tari_console_wallet/src/ui/components/tabs_container.rs b/applications/tari_console_wallet/src/ui/components/tabs_container.rs index 7fb9380c6b..512e810df2 100644 --- a/applications/tari_console_wallet/src/ui/components/tabs_container.rs +++ b/applications/tari_console_wallet/src/ui/components/tabs_container.rs @@ -91,7 +91,6 @@ impl TabsContainer { impl Component for TabsContainer { fn draw(&mut self, _: &mut Frame, _: Rect, _: &AppState) { // Use draw_titles and draw_content instead, - // TODO: Create a layout and draw both unimplemented!() } diff --git a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs index 315dad68cc..3d1117c732 100644 --- a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use chrono::{DateTime, Local}; +use log::*; use tari_common_types::transaction::{TransactionDirection, TransactionStatus}; use tokio::runtime::Handle; use tui::{ @@ -19,6 +20,8 @@ use crate::ui::{ MAX_WIDTH, }; +const LOG_TARGET: &str = "wallet::console_wallet::transaction_tab"; + pub struct TransactionsTab { balance: Balance, selected_tx_list: SelectedTransactionList, @@ -571,8 +574,9 @@ impl Component for TransactionsTab { }, // Rebroadcast 'r' => { - // TODO: use this result - let _res = Handle::current().block_on(app_state.rebroadcast_all()); + if let Err(e) = Handle::current().block_on(app_state.rebroadcast_all()) { + error!(target: LOG_TARGET, "Error rebroadcasting transactions: {}", e); + } }, 'a' => app_state.toggle_abandoned_coinbase_filter(), '\n' => match self.selected_tx_list { diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index 8a77875a53..955762b728 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -248,10 +248,7 @@ impl AppState { // Return alias or pub key if the contact is not in the list. pub fn get_alias(&self, pub_key: &RistrettoPublicKey) -> String { let pub_key_hex = format!("{}", pub_key); - // TODO: We can uncomment this to indicated unknown origin, otherwise there is our pub key. - // if self.get_identity().public_key == pub_key_hex { - // return "Unknown".to_string(); - // } + match self .cached_data .contacts @@ -533,7 +530,7 @@ impl AppState { pub fn get_default_fee_per_gram(&self) -> MicroTari { use Network::*; - // TODO: TBD + // TODO: TBD #LOGGED match self.node_config.network { MainNet | LocalNet | Igor | Dibbler => MicroTari(5), Ridcully | Stibbons | Weatherwax => MicroTari(25), diff --git a/applications/tari_merge_mining_proxy/src/common/merge_mining.rs b/applications/tari_merge_mining_proxy/src/common/merge_mining.rs index 0b13b1a1f1..7b85ec919d 100644 --- a/applications/tari_merge_mining_proxy/src/common/merge_mining.rs +++ b/applications/tari_merge_mining_proxy/src/common/merge_mining.rs @@ -25,7 +25,7 @@ use std::convert::{TryFrom, TryInto}; use tari_app_grpc::tari_rpc as grpc; use tari_core::{ blocks::NewBlockTemplate, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, }; use crate::error::MmProxyError; diff --git a/applications/tari_merge_mining_proxy/src/common/proxy.rs b/applications/tari_merge_mining_proxy/src/common/proxy.rs index 693714561b..61d9d1fd9c 100644 --- a/applications/tari_merge_mining_proxy/src/common/proxy.rs +++ b/applications/tari_merge_mining_proxy/src/common/proxy.rs @@ -76,7 +76,6 @@ pub fn into_body_from_response(resp: Response) -> Response { /// Reads the `Body` until there is no more to read pub async fn read_body_until_end(body: &mut Body) -> Result { - // TODO: Perhaps there is a more efficient way to do this let mut bytes = BytesMut::new(); while let Some(data) = body.next().await { let data = data?; diff --git a/applications/tari_stratum_transcoder/src/common/mining.rs b/applications/tari_stratum_transcoder/src/common/mining.rs index f3d7842628..97b88d6336 100644 --- a/applications/tari_stratum_transcoder/src/common/mining.rs +++ b/applications/tari_stratum_transcoder/src/common/mining.rs @@ -25,7 +25,7 @@ use std::convert::TryFrom; use tari_app_grpc::tari_rpc as grpc; use tari_core::{ blocks::NewBlockTemplate, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, }; use crate::error::StratumTranscoderProxyError; diff --git a/applications/tari_stratum_transcoder/src/common/proxy.rs b/applications/tari_stratum_transcoder/src/common/proxy.rs index b9553c6278..fae8d300b3 100644 --- a/applications/tari_stratum_transcoder/src/common/proxy.rs +++ b/applications/tari_stratum_transcoder/src/common/proxy.rs @@ -56,7 +56,6 @@ pub fn into_body_from_response(resp: Response) -> Response { /// Reads the `Body` until there is no more to read pub async fn read_body_until_end(body: &mut Body) -> Result { - // TODO: Perhaps there is a more efficient way to do this let mut bytes = BytesMut::new(); while let Some(data) = body.next().await { let data = data?; diff --git a/applications/test_faucet/src/main.rs b/applications/test_faucet/src/main.rs index 5667055f3f..a8d0d1ae8f 100644 --- a/applications/test_faucet/src/main.rs +++ b/applications/test_faucet/src/main.rs @@ -19,7 +19,7 @@ use tari_core::{ tari_amount::{MicroTari, T}, test_helpers, test_helpers::generate_keys, - transaction::{KernelFeatures, OutputFeatures, TransactionKernel, TransactionOutput}, + transaction_components::{KernelFeatures, OutputFeatures, TransactionKernel, TransactionOutput}, CryptoFactories, }, }; diff --git a/base_layer/core/src/base_node/comms_interface/comms_response.rs b/base_layer/core/src/base_node/comms_interface/comms_response.rs index a5a0e3c90f..2089ac2155 100644 --- a/base_layer/core/src/base_node/comms_interface/comms_response.rs +++ b/base_layer/core/src/base_node/comms_interface/comms_response.rs @@ -34,7 +34,7 @@ use crate::{ blocks::{Block, BlockHeader, ChainHeader, HistoricalBlock, NewBlockTemplate}, chain_storage::UtxoMinedInfo, proof_of_work::Difficulty, - transactions::transaction::{Transaction, TransactionKernel, TransactionOutput}, + transactions::transaction_components::{Transaction, TransactionKernel, TransactionOutput}, }; /// API Response enum diff --git a/base_layer/core/src/base_node/comms_interface/local_interface.rs b/base_layer/core/src/base_node/comms_interface/local_interface.rs index 4bd623cc46..620159bc36 100644 --- a/base_layer/core/src/base_node/comms_interface/local_interface.rs +++ b/base_layer/core/src/base_node/comms_interface/local_interface.rs @@ -40,7 +40,7 @@ use crate::{ blocks::{Block, ChainHeader, HistoricalBlock, NewBlockTemplate}, chain_storage::UtxoMinedInfo, proof_of_work::PowAlgorithm, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, }; pub type BlockEventSender = broadcast::Sender>; diff --git a/base_layer/core/src/base_node/rpc/service.rs b/base_layer/core/src/base_node/rpc/service.rs index 3a0de04db1..9eee31bb23 100644 --- a/base_layer/core/src/base_node/rpc/service.rs +++ b/base_layer/core/src/base_node/rpc/service.rs @@ -60,7 +60,7 @@ use crate::{ }, types::{Signature as SignatureProto, Transaction as TransactionProto}, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; const LOG_TARGET: &str = "c::base_node::rpc"; @@ -432,7 +432,6 @@ impl BaseNodeWalletService for BaseNodeWalletRpc for position in message.mmr_positions { if position > u32::MAX as u64 { - // TODO: in future, bitmap may support higher than u32 return Err(RpcStatus::bad_request("position must fit into a u32")); } let position = position as u32; diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs index 8844d37486..236e8476d4 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/error.rs @@ -33,7 +33,7 @@ use tokio::task; use crate::{ base_node::{comms_interface::CommsInterfaceError, state_machine_service::states::helpers::BaseNodeRequestError}, chain_storage::{ChainStorageError, MmrTree}, - transactions::transaction::TransactionError, + transactions::transaction_components::TransactionError, validation::ValidationError, }; diff --git a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs index 50800cd876..0f31bbf095 100644 --- a/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs +++ b/base_layer/core/src/base_node/state_machine_service/states/horizon_state_sync/horizon_state_synchronization.rs @@ -65,7 +65,7 @@ use crate::{ SyncUtxosRequest, SyncUtxosResponse, }, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, validation::helpers, }; diff --git a/base_layer/core/src/blocks/block.rs b/base_layer/core/src/blocks/block.rs index 998935ac91..25b3daee62 100644 --- a/base_layer/core/src/blocks/block.rs +++ b/base_layer/core/src/blocks/block.rs @@ -42,7 +42,7 @@ use crate::{ transactions::{ aggregated_body::AggregateBody, tari_amount::MicroTari, - transaction::{ + transaction_components::{ KernelFeatures, OutputFlags, Transaction, diff --git a/base_layer/core/src/blocks/genesis_block.rs b/base_layer/core/src/blocks/genesis_block.rs index 779a8ffeb6..f2e4f18501 100644 --- a/base_layer/core/src/blocks/genesis_block.rs +++ b/base_layer/core/src/blocks/genesis_block.rs @@ -37,7 +37,7 @@ use crate::{ transactions::{ aggregated_body::AggregateBody, tari_amount::MicroTari, - transaction::{KernelFeatures, OutputFeatures, OutputFlags, TransactionKernel, TransactionOutput}, + transaction_components::{KernelFeatures, OutputFeatures, OutputFlags, TransactionKernel, TransactionOutput}, }, }; diff --git a/base_layer/core/src/chain_storage/async_db.rs b/base_layer/core/src/chain_storage/async_db.rs index 621ef100b6..5e7b65bf14 100644 --- a/base_layer/core/src/chain_storage/async_db.rs +++ b/base_layer/core/src/chain_storage/async_db.rs @@ -66,7 +66,7 @@ use crate::{ }, common::rolling_vec::RollingVec, proof_of_work::{PowAlgorithm, TargetDifficultyWindow}, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, }; const LOG_TARGET: &str = "c::bn::async_db"; diff --git a/base_layer/core/src/chain_storage/blockchain_backend.rs b/base_layer/core/src/chain_storage/blockchain_backend.rs index d0dc0a7d4d..30dcec4b27 100644 --- a/base_layer/core/src/chain_storage/blockchain_backend.rs +++ b/base_layer/core/src/chain_storage/blockchain_backend.rs @@ -30,7 +30,7 @@ use crate::{ Reorg, UtxoMinedInfo, }, - transactions::transaction::{TransactionInput, TransactionKernel}, + transactions::transaction_components::{TransactionInput, TransactionKernel}, }; /// Identify behaviour for Blockchain database backends. Implementations must support `Send` and `Sync` so that diff --git a/base_layer/core/src/chain_storage/blockchain_database.rs b/base_layer/core/src/chain_storage/blockchain_database.rs index 15bbafbcd2..53366d21bf 100644 --- a/base_layer/core/src/chain_storage/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/blockchain_database.rs @@ -79,7 +79,7 @@ use crate::{ common::rolling_vec::RollingVec, consensus::{chain_strength_comparer::ChainStrengthComparer, ConsensusConstants, ConsensusManager}, proof_of_work::{monero_rx::MoneroPowData, PowAlgorithm, TargetDifficultyWindow}, - transactions::transaction::{TransactionInput, TransactionKernel}, + transactions::transaction_components::{TransactionInput, TransactionKernel}, validation::{ helpers::calc_median_timestamp, DifficultyCalculator, diff --git a/base_layer/core/src/chain_storage/db_transaction.rs b/base_layer/core/src/chain_storage/db_transaction.rs index 88dd11cf47..a2ce4c5d92 100644 --- a/base_layer/core/src/chain_storage/db_transaction.rs +++ b/base_layer/core/src/chain_storage/db_transaction.rs @@ -36,7 +36,7 @@ use tari_crypto::tari_utilities::{ use crate::{ blocks::{Block, BlockHeader, BlockHeaderAccumulatedData, ChainBlock, ChainHeader, UpdateBlockAccumulatedData}, chain_storage::{error::ChainStorageError, HorizonData, Reorg}, - transactions::transaction::{TransactionKernel, TransactionOutput}, + transactions::transaction_components::{TransactionKernel, TransactionOutput}, }; #[derive(Debug)] diff --git a/base_layer/core/src/chain_storage/error.rs b/base_layer/core/src/chain_storage/error.rs index 4a0cee3645..65be9628c2 100644 --- a/base_layer/core/src/chain_storage/error.rs +++ b/base_layer/core/src/chain_storage/error.rs @@ -30,7 +30,7 @@ use crate::{ blocks::BlockError, chain_storage::MmrTree, proof_of_work::PowError, - transactions::transaction::TransactionError, + transactions::transaction_components::TransactionError, validation::ValidationError, }; diff --git a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs index 7bbb2f67e9..8dde5cdb43 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/lmdb_db.rs @@ -104,7 +104,7 @@ use crate::{ }, transactions::{ aggregated_body::AggregateBody, - transaction::{TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, + transaction_components::{TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, }, }; diff --git a/base_layer/core/src/chain_storage/lmdb_db/mod.rs b/base_layer/core/src/chain_storage/lmdb_db/mod.rs index e01ed62c58..ad8b12c831 100644 --- a/base_layer/core/src/chain_storage/lmdb_db/mod.rs +++ b/base_layer/core/src/chain_storage/lmdb_db/mod.rs @@ -24,7 +24,7 @@ pub use lmdb_db::{create_lmdb_database, create_recovery_lmdb_database, LMDBDatab use serde::{Deserialize, Serialize}; use tari_common_types::types::HashOutput; -use crate::transactions::transaction::{TransactionInput, TransactionKernel, TransactionOutput}; +use crate::transactions::transaction_components::{TransactionInput, TransactionKernel, TransactionOutput}; pub(crate) mod helpers; pub(crate) mod key_prefix_cursor; diff --git a/base_layer/core/src/chain_storage/pruned_output.rs b/base_layer/core/src/chain_storage/pruned_output.rs index 6686285214..e505963e77 100644 --- a/base_layer/core/src/chain_storage/pruned_output.rs +++ b/base_layer/core/src/chain_storage/pruned_output.rs @@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize}; use tari_common_types::types::HashOutput; use tari_crypto::tari_utilities::Hashable; -use crate::transactions::transaction::TransactionOutput; +use crate::transactions::transaction_components::TransactionOutput; #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/base_layer/core/src/chain_storage/tests/blockchain_database.rs b/base_layer/core/src/chain_storage/tests/blockchain_database.rs index 5bb31b4d19..ea825f0f8e 100644 --- a/base_layer/core/src/chain_storage/tests/blockchain_database.rs +++ b/base_layer/core/src/chain_storage/tests/blockchain_database.rs @@ -40,7 +40,7 @@ use crate::{ transactions::{ tari_amount::T, test_helpers::{schema_to_transaction, TransactionSchema}, - transaction::{OutputFeatures, OutputFlags, Transaction, UnblindedOutput}, + transaction_components::{OutputFeatures, OutputFlags, Transaction, UnblindedOutput}, }, txn_schema, }; @@ -708,7 +708,7 @@ mod fetch_utxo_by_unique_id { use tari_crypto::{commitment::HomomorphicCommitmentFactory, ristretto::RistrettoPublicKey}; use super::*; - use crate::transactions::transaction::OutputFlags; + use crate::transactions::transaction_components::OutputFlags; #[test] fn it_returns_none_if_empty() { diff --git a/base_layer/core/src/consensus/consensus_constants.rs b/base_layer/core/src/consensus/consensus_constants.rs index fa272cd723..4da890d10d 100644 --- a/base_layer/core/src/consensus/consensus_constants.rs +++ b/base_layer/core/src/consensus/consensus_constants.rs @@ -31,7 +31,7 @@ use crate::{ proof_of_work::{Difficulty, PowAlgorithm}, transactions::{ tari_amount::{uT, MicroTari, T}, - transaction::OutputFeatures, + transaction_components::OutputFeatures, weight::TransactionWeight, }, }; diff --git a/base_layer/core/src/consensus/consensus_manager.rs b/base_layer/core/src/consensus/consensus_manager.rs index e6bf9206d9..c4c01b0f2a 100644 --- a/base_layer/core/src/consensus/consensus_manager.rs +++ b/base_layer/core/src/consensus/consensus_manager.rs @@ -39,7 +39,7 @@ use crate::{ NetworkConsensus, }, proof_of_work::DifficultyAdjustmentError, - transactions::{tari_amount::MicroTari, transaction::TransactionKernel}, + transactions::{tari_amount::MicroTari, transaction_components::TransactionKernel}, }; #[derive(Debug, Error)] diff --git a/base_layer/core/src/covenants/context.rs b/base_layer/core/src/covenants/context.rs index c3a17b3df0..c9df25e984 100644 --- a/base_layer/core/src/covenants/context.rs +++ b/base_layer/core/src/covenants/context.rs @@ -27,7 +27,7 @@ use crate::{ filters::CovenantFilter, token::{CovenantToken, CovenantTokenCollection}, }, - transactions::transaction::TransactionInput, + transactions::transaction_components::TransactionInput, }; pub struct CovenantContext<'a> { diff --git a/base_layer/core/src/covenants/covenant.rs b/base_layer/core/src/covenants/covenant.rs index 5f993b166f..6dedb52883 100644 --- a/base_layer/core/src/covenants/covenant.rs +++ b/base_layer/core/src/covenants/covenant.rs @@ -36,7 +36,7 @@ use crate::{ output_set::OutputSet, token::{CovenantToken, CovenantTokenCollection}, }, - transactions::transaction::{TransactionInput, TransactionOutput}, + transactions::transaction_components::{TransactionInput, TransactionOutput}, }; const MAX_COVENANT_BYTES: usize = 4096; diff --git a/base_layer/core/src/covenants/fields.rs b/base_layer/core/src/covenants/fields.rs index 6bfb7048fe..1c03c7bbb6 100644 --- a/base_layer/core/src/covenants/fields.rs +++ b/base_layer/core/src/covenants/fields.rs @@ -39,7 +39,7 @@ use crate::{ encoder::CovenentWriteExt, error::CovenantError, }, - transactions::transaction::{TransactionInput, TransactionOutput}, + transactions::transaction_components::{TransactionInput, TransactionOutput}, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -330,7 +330,7 @@ mod test { use super::*; use crate::{ covenants::test::create_outputs, - transactions::{test_helpers::UtxoTestParams, transaction::OutputFeatures}, + transactions::{test_helpers::UtxoTestParams, transaction_components::OutputFeatures}, }; #[test] diff --git a/base_layer/core/src/covenants/filters/fields_hashed_eq.rs b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs index 9ea32e2077..3fd6816b44 100644 --- a/base_layer/core/src/covenants/filters/fields_hashed_eq.rs +++ b/base_layer/core/src/covenants/filters/fields_hashed_eq.rs @@ -70,7 +70,7 @@ mod test { consensus::ToConsensusBytes, covenant, covenants::{filters::test::setup_filter_test, test::create_input}, - transactions::transaction::OutputFeatures, + transactions::transaction_components::OutputFeatures, }; #[test] diff --git a/base_layer/core/src/covenants/filters/fields_preserved.rs b/base_layer/core/src/covenants/filters/fields_preserved.rs index 8f38810273..0872df308b 100644 --- a/base_layer/core/src/covenants/filters/fields_preserved.rs +++ b/base_layer/core/src/covenants/filters/fields_preserved.rs @@ -41,7 +41,7 @@ mod test { use crate::{ covenant, covenants::{filters::test::setup_filter_test, test::create_input}, - transactions::transaction::OutputFlags, + transactions::transaction_components::OutputFlags, }; #[test] diff --git a/base_layer/core/src/covenants/filters/test.rs b/base_layer/core/src/covenants/filters/test.rs index 5169a14abb..38a30381ae 100644 --- a/base_layer/core/src/covenants/filters/test.rs +++ b/base_layer/core/src/covenants/filters/test.rs @@ -26,7 +26,7 @@ use crate::{ test::{create_context, create_outputs}, Covenant, }, - transactions::transaction::{TransactionInput, TransactionOutput}, + transactions::transaction_components::{TransactionInput, TransactionOutput}, }; pub fn setup_filter_test<'a, F>( diff --git a/base_layer/core/src/covenants/output_set.rs b/base_layer/core/src/covenants/output_set.rs index f086214614..30e5aa2a7a 100644 --- a/base_layer/core/src/covenants/output_set.rs +++ b/base_layer/core/src/covenants/output_set.rs @@ -27,7 +27,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::{covenants::error::CovenantError, transactions::transaction::TransactionOutput}; +use crate::{covenants::error::CovenantError, transactions::transaction_components::TransactionOutput}; #[derive(Debug, Clone)] pub struct OutputSet<'a>(BTreeSet>); diff --git a/base_layer/core/src/covenants/test.rs b/base_layer/core/src/covenants/test.rs index f881bd7445..96a700ca99 100644 --- a/base_layer/core/src/covenants/test.rs +++ b/base_layer/core/src/covenants/test.rs @@ -26,7 +26,7 @@ use crate::{ covenants::{context::CovenantContext, Covenant}, transactions::{ test_helpers::{TestParams, UtxoTestParams}, - transaction::{TransactionInput, TransactionOutput}, + transaction_components::{TransactionInput, TransactionOutput}, }, }; diff --git a/base_layer/core/src/mempool/error.rs b/base_layer/core/src/mempool/error.rs index f0d09db1d7..1d96ec21b9 100644 --- a/base_layer/core/src/mempool/error.rs +++ b/base_layer/core/src/mempool/error.rs @@ -24,7 +24,7 @@ use tari_service_framework::reply_channel::TransportChannelError; use thiserror::Error; use tokio::task::JoinError; -use crate::{mempool::unconfirmed_pool::UnconfirmedPoolError, transactions::transaction::TransactionError}; +use crate::{mempool::unconfirmed_pool::UnconfirmedPoolError, transactions::transaction_components::TransactionError}; #[derive(Debug, Error)] pub enum MempoolError { diff --git a/base_layer/core/src/mempool/mempool.rs b/base_layer/core/src/mempool/mempool.rs index 079aa8e21a..af6bcc691e 100644 --- a/base_layer/core/src/mempool/mempool.rs +++ b/base_layer/core/src/mempool/mempool.rs @@ -36,7 +36,7 @@ use crate::{ StatsResponse, TxStorageResponse, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, validation::MempoolTransactionValidation, }; diff --git a/base_layer/core/src/mempool/mempool_storage.rs b/base_layer/core/src/mempool/mempool_storage.rs index dc6a6a2c17..52c97dfd86 100644 --- a/base_layer/core/src/mempool/mempool_storage.rs +++ b/base_layer/core/src/mempool/mempool_storage.rs @@ -38,7 +38,7 @@ use crate::{ StatsResponse, TxStorageResponse, }, - transactions::{transaction::Transaction, weight::TransactionWeight}, + transactions::{transaction_components::Transaction, weight::TransactionWeight}, validation::{MempoolTransactionValidation, ValidationError}, }; diff --git a/base_layer/core/src/mempool/mod.rs b/base_layer/core/src/mempool/mod.rs index 9b00310186..52dddc432f 100644 --- a/base_layer/core/src/mempool/mod.rs +++ b/base_layer/core/src/mempool/mod.rs @@ -77,7 +77,7 @@ pub use sync_protocol::MempoolSyncInitializer; use tari_common_types::types::Signature; use tari_crypto::tari_utilities::hex::Hex; -use crate::transactions::transaction::Transaction; +use crate::transactions::transaction_components::Transaction; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct StatsResponse { diff --git a/base_layer/core/src/mempool/priority/prioritized_transaction.rs b/base_layer/core/src/mempool/priority/prioritized_transaction.rs index 381717ba02..7f48128f38 100644 --- a/base_layer/core/src/mempool/priority/prioritized_transaction.rs +++ b/base_layer/core/src/mempool/priority/prioritized_transaction.rs @@ -28,7 +28,7 @@ use std::{ use tari_common_types::types::{HashOutput, PrivateKey, PublicKey}; use tari_utilities::{hex::Hex, ByteArray}; -use crate::transactions::{transaction::Transaction, weight::TransactionWeight}; +use crate::transactions::{transaction_components::Transaction, weight::TransactionWeight}; /// Create a unique unspent transaction priority based on the transaction fee, maturity of the oldest input UTXO and the /// excess_sig. The excess_sig is included to ensure the the priority key unique so it can be used with a BTreeMap. diff --git a/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs b/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs index bcd68e4b69..c91d58583e 100644 --- a/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs +++ b/base_layer/core/src/mempool/reorg_pool/reorg_pool.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use tari_common_types::types::{PrivateKey, Signature}; use tari_utilities::{hex::Hex, Hashable}; -use crate::{blocks::Block, transactions::transaction::Transaction}; +use crate::{blocks::Block, transactions::transaction_components::Transaction}; pub const LOG_TARGET: &str = "c::mp::reorg_pool::reorg_pool_storage"; diff --git a/base_layer/core/src/mempool/rpc/service.rs b/base_layer/core/src/mempool/rpc/service.rs index cb9f199f51..b82add1df9 100644 --- a/base_layer/core/src/mempool/rpc/service.rs +++ b/base_layer/core/src/mempool/rpc/service.rs @@ -28,7 +28,7 @@ use tari_comms::protocol::rpc::{Request, Response, RpcStatus}; use crate::{ mempool::{rpc::MempoolService, service::MempoolHandle}, proto, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; const LOG_TARGET: &str = "c::mempool::rpc"; diff --git a/base_layer/core/src/mempool/service/handle.rs b/base_layer/core/src/mempool/service/handle.rs index 96aaac6f76..c76a6b54a2 100644 --- a/base_layer/core/src/mempool/service/handle.rs +++ b/base_layer/core/src/mempool/service/handle.rs @@ -31,7 +31,7 @@ use crate::{ StatsResponse, TxStorageResponse, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; #[derive(Clone)] diff --git a/base_layer/core/src/mempool/service/inbound_handlers.rs b/base_layer/core/src/mempool/service/inbound_handlers.rs index eeecc00ebc..4388d15d16 100644 --- a/base_layer/core/src/mempool/service/inbound_handlers.rs +++ b/base_layer/core/src/mempool/service/inbound_handlers.rs @@ -35,7 +35,7 @@ use crate::{ Mempool, TxStorageResponse, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; pub const LOG_TARGET: &str = "c::mp::service::inbound_handlers"; diff --git a/base_layer/core/src/mempool/service/initializer.rs b/base_layer/core/src/mempool/service/initializer.rs index 5eaf65a149..83431f8f19 100644 --- a/base_layer/core/src/mempool/service/initializer.rs +++ b/base_layer/core/src/mempool/service/initializer.rs @@ -52,7 +52,7 @@ use crate::{ }, }, proto, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; const LOG_TARGET: &str = "c::bn::mempool_service::initializer"; diff --git a/base_layer/core/src/mempool/service/local_service.rs b/base_layer/core/src/mempool/service/local_service.rs index ae6645da98..cb236146df 100644 --- a/base_layer/core/src/mempool/service/local_service.rs +++ b/base_layer/core/src/mempool/service/local_service.rs @@ -30,7 +30,7 @@ use crate::{ StatsResponse, TxStorageResponse, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; pub type LocalMempoolRequester = SenderService>; diff --git a/base_layer/core/src/mempool/service/outbound_interface.rs b/base_layer/core/src/mempool/service/outbound_interface.rs index 7eb1a4bc2a..bedac7116a 100644 --- a/base_layer/core/src/mempool/service/outbound_interface.rs +++ b/base_layer/core/src/mempool/service/outbound_interface.rs @@ -26,7 +26,7 @@ use log::*; use tari_comms::peer_manager::NodeId; use tokio::sync::mpsc::UnboundedSender; -use crate::{mempool::service::MempoolServiceError, transactions::transaction::Transaction}; +use crate::{mempool::service::MempoolServiceError, transactions::transaction_components::Transaction}; pub const LOG_TARGET: &str = "c::mp::service::outbound_interface"; diff --git a/base_layer/core/src/mempool/service/request.rs b/base_layer/core/src/mempool/service/request.rs index 43941aa7db..c6a3bf783d 100644 --- a/base_layer/core/src/mempool/service/request.rs +++ b/base_layer/core/src/mempool/service/request.rs @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize}; use tari_common_types::{types::Signature, waiting_requests::RequestKey}; use tari_crypto::tari_utilities::hex::Hex; -use crate::transactions::transaction::Transaction; +use crate::transactions::transaction_components::Transaction; /// API Request enum for Mempool requests. #[derive(Debug, Serialize, Deserialize)] diff --git a/base_layer/core/src/mempool/service/service.rs b/base_layer/core/src/mempool/service/service.rs index 28168febf0..ee99073504 100644 --- a/base_layer/core/src/mempool/service/service.rs +++ b/base_layer/core/src/mempool/service/service.rs @@ -47,7 +47,7 @@ use crate::{ MempoolResponse, }, proto, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; const LOG_TARGET: &str = "c::mempool::service::service"; diff --git a/base_layer/core/src/mempool/sync_protocol/mod.rs b/base_layer/core/src/mempool/sync_protocol/mod.rs index 488fcd28e0..08ec64c455 100644 --- a/base_layer/core/src/mempool/sync_protocol/mod.rs +++ b/base_layer/core/src/mempool/sync_protocol/mod.rs @@ -100,7 +100,7 @@ use tokio::{ use crate::{ mempool::{metrics, proto, Mempool, MempoolServiceConfig}, proto as shared_proto, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; #[cfg(test)] diff --git a/base_layer/core/src/mempool/sync_protocol/test.rs b/base_layer/core/src/mempool/sync_protocol/test.rs index 0e268df30e..623e63a5d7 100644 --- a/base_layer/core/src/mempool/sync_protocol/test.rs +++ b/base_layer/core/src/mempool/sync_protocol/test.rs @@ -48,7 +48,7 @@ use crate::{ sync_protocol::{MempoolPeerProtocol, MempoolSyncProtocol, MAX_FRAME_SIZE, MEMPOOL_SYNC_PROTOCOL}, Mempool, }, - transactions::{tari_amount::uT, test_helpers::create_tx, transaction::Transaction}, + transactions::{tari_amount::uT, test_helpers::create_tx, transaction_components::Transaction}, validation::mocks::MockValidator, }; diff --git a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs index 3450d20368..a8fe8ed055 100644 --- a/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs +++ b/base_layer/core/src/mempool/unconfirmed_pool/unconfirmed_pool.rs @@ -41,7 +41,7 @@ use crate::{ unconfirmed_pool::UnconfirmedPoolError, }, transactions::{ - transaction::{Transaction, TransactionOutput}, + transaction_components::{Transaction, TransactionOutput}, weight::TransactionWeight, }, }; @@ -628,7 +628,7 @@ mod test { fee::Fee, tari_amount::MicroTari, test_helpers::{TestParams, UtxoTestParams}, - transaction::{KernelFeatures, OutputFeatures}, + transaction_components::{KernelFeatures, OutputFeatures}, weight::TransactionWeight, CryptoFactories, SenderTransactionProtocol, diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index b212aa1d9a..af4986efc3 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -40,7 +40,7 @@ use crate::{ transactions::{ aggregated_body::AggregateBody, tari_amount::MicroTari, - transaction::{ + transaction_components::{ AssetOutputFeatures, KernelFeatures, MintNonFungibleFeatures, diff --git a/base_layer/core/src/test_helpers/block_spec.rs b/base_layer/core/src/test_helpers/block_spec.rs index c590ca198d..0c28ad7532 100644 --- a/base_layer/core/src/test_helpers/block_spec.rs +++ b/base_layer/core/src/test_helpers/block_spec.rs @@ -22,7 +22,7 @@ use crate::{ proof_of_work::Difficulty, - transactions::{tari_amount::MicroTari, transaction::Transaction}, + transactions::{tari_amount::MicroTari, transaction_components::Transaction}, }; pub struct BlockSpecs { diff --git a/base_layer/core/src/test_helpers/blockchain.rs b/base_layer/core/src/test_helpers/blockchain.rs index c8fc66b6c3..229fb3f1e4 100644 --- a/base_layer/core/src/test_helpers/blockchain.rs +++ b/base_layer/core/src/test_helpers/blockchain.rs @@ -73,7 +73,7 @@ use crate::{ proof_of_work::{AchievedTargetDifficulty, Difficulty, PowAlgorithm}, test_helpers::{block_spec::BlockSpecs, create_consensus_rules, BlockSpec}, transactions::{ - transaction::{TransactionInput, TransactionKernel, UnblindedOutput}, + transaction_components::{TransactionInput, TransactionKernel, UnblindedOutput}, CryptoFactories, }, validation::{ diff --git a/base_layer/core/src/test_helpers/mod.rs b/base_layer/core/src/test_helpers/mod.rs index 503a044c49..d85d8b7436 100644 --- a/base_layer/core/src/test_helpers/mod.rs +++ b/base_layer/core/src/test_helpers/mod.rs @@ -37,7 +37,7 @@ use crate::{ consensus::{ConsensusConstants, ConsensusManager}, proof_of_work::{sha3_difficulty, AchievedTargetDifficulty, Difficulty}, transactions::{ - transaction::{Transaction, UnblindedOutput}, + transaction_components::{Transaction, UnblindedOutput}, CoinbaseBuilder, CryptoFactories, }, diff --git a/base_layer/core/src/transactions/aggregated_body.rs b/base_layer/core/src/transactions/aggregated_body.rs index dfe53734c7..53b583276c 100644 --- a/base_layer/core/src/transactions/aggregated_body.rs +++ b/base_layer/core/src/transactions/aggregated_body.rs @@ -47,7 +47,7 @@ use tari_crypto::{ use crate::transactions::{ crypto_factories::CryptoFactories, tari_amount::MicroTari, - transaction::{ + transaction_components::{ KernelFeatures, KernelSum, OutputFlags, diff --git a/base_layer/core/src/transactions/coinbase_builder.rs b/base_layer/core/src/transactions/coinbase_builder.rs index 724b07ef88..dfa08f221a 100644 --- a/base_layer/core/src/transactions/coinbase_builder.rs +++ b/base_layer/core/src/transactions/coinbase_builder.rs @@ -41,7 +41,7 @@ use crate::{ transactions::{ crypto_factories::CryptoFactories, tari_amount::{uT, MicroTari}, - transaction::{ + transaction_components::{ KernelBuilder, KernelFeatures, OutputFeatures, @@ -222,7 +222,6 @@ impl CoinbaseBuilder { 0, covenant, ); - // TODO: Verify bullet proof? let output = if let Some(rewind_data) = self.rewind_data.as_ref() { unblinded_output .as_rewindable_transaction_output(&self.factories, rewind_data, None) @@ -269,7 +268,7 @@ mod test { crypto_factories::CryptoFactories, tari_amount::uT, test_helpers::TestParams, - transaction::{KernelFeatures, OutputFeatures, OutputFlags, TransactionError}, + transaction_components::{KernelFeatures, OutputFeatures, OutputFlags, TransactionError}, transaction_protocol::RewindData, CoinbaseBuilder, }, diff --git a/base_layer/core/src/transactions/mod.rs b/base_layer/core/src/transactions/mod.rs index ee63b708c2..ec1717a870 100644 --- a/base_layer/core/src/transactions/mod.rs +++ b/base_layer/core/src/transactions/mod.rs @@ -8,7 +8,7 @@ pub use coinbase_builder::{CoinbaseBuildError, CoinbaseBuilder}; pub mod fee; pub mod tari_amount; -pub mod transaction; +pub mod transaction_components; mod format_currency; pub use format_currency::format_currency; diff --git a/base_layer/core/src/transactions/tari_amount.rs b/base_layer/core/src/transactions/tari_amount.rs index 06dca76185..472d036546 100644 --- a/base_layer/core/src/transactions/tari_amount.rs +++ b/base_layer/core/src/transactions/tari_amount.rs @@ -146,8 +146,7 @@ impl std::str::FromStr for MicroTari { if is_micro_tari { processed .parse::() - // TODO: Why we compare it with `0` here? It's unsigned. - .map(|v| MicroTari::from(v.max(0))) + .map(MicroTari::from) .map_err(|e| MicroTariError::ParseError(e.to_string())) } else { processed @@ -157,7 +156,6 @@ impl std::str::FromStr for MicroTari { if v.is_sign_negative() { Err(MicroTariError::ParseError("value cannot be negative".to_string())) } else { - // TODO: Check. It can't be `NaN` anymore. Still we need `.max(0.0)` check? Tari::from(v).try_into().map_err(MicroTariError::from) } })? @@ -262,7 +260,7 @@ impl Display for Tari { pub type TariConversionError = DecimalConvertError; -// TODO: Remove `f64` completely! Using it is the bad idea in general. +// TODO: Remove `f64` completely! Using it is the bad idea in general. #LOGGED impl TryFrom for Tari { type Error = TariConversionError; diff --git a/base_layer/core/src/transactions/test_helpers.rs b/base_layer/core/src/transactions/test_helpers.rs index 544215df6b..39349a54f9 100644 --- a/base_layer/core/src/transactions/test_helpers.rs +++ b/base_layer/core/src/transactions/test_helpers.rs @@ -42,7 +42,7 @@ use crate::{ crypto_factories::CryptoFactories, fee::Fee, tari_amount::MicroTari, - transaction::{ + transaction_components::{ KernelBuilder, KernelFeatures, OutputFeatures, @@ -334,7 +334,7 @@ macro_rules! txn_schema { to:$outputs, fee:$fee, lock:0, - features: $crate::transactions::transaction::OutputFeatures::default() + features: $crate::transactions::transaction_components::OutputFeatures::default() ) }; diff --git a/base_layer/core/src/transactions/transaction/asset_output_features.rs b/base_layer/core/src/transactions/transaction_components/asset_output_features.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/asset_output_features.rs rename to base_layer/core/src/transactions/transaction_components/asset_output_features.rs index 7901727232..95de23cba4 100644 --- a/base_layer/core/src/transactions/transaction/asset_output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/asset_output_features.rs @@ -30,7 +30,7 @@ use tari_common_types::types::PublicKey; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeVec}, - transactions::transaction::TemplateParameter, + transactions::transaction_components::TemplateParameter, }; #[derive(Debug, Clone, Hash, PartialEq, Deserialize, Serialize, Eq)] diff --git a/base_layer/core/src/transactions/transaction/error.rs b/base_layer/core/src/transactions/transaction_components/error.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/error.rs rename to base_layer/core/src/transactions/transaction_components/error.rs diff --git a/base_layer/core/src/transactions/transaction/full_rewind_result.rs b/base_layer/core/src/transactions/transaction_components/full_rewind_result.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/full_rewind_result.rs rename to base_layer/core/src/transactions/transaction_components/full_rewind_result.rs diff --git a/base_layer/core/src/transactions/transaction/kernel_builder.rs b/base_layer/core/src/transactions/transaction_components/kernel_builder.rs similarity index 97% rename from base_layer/core/src/transactions/transaction/kernel_builder.rs rename to base_layer/core/src/transactions/transaction_components/kernel_builder.rs index e4c568a02a..08b9e9623f 100644 --- a/base_layer/core/src/transactions/transaction/kernel_builder.rs +++ b/base_layer/core/src/transactions/transaction_components/kernel_builder.rs @@ -27,7 +27,7 @@ use tari_common_types::types::{Commitment, Signature}; use crate::transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, TransactionError, TransactionKernel}, + transaction_components::{KernelFeatures, TransactionError, TransactionKernel}, }; /// A version of Transaction kernel with optional fields. This struct is only used in constructing transaction kernels diff --git a/base_layer/core/src/transactions/transaction/kernel_features.rs b/base_layer/core/src/transactions/transaction_components/kernel_features.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/kernel_features.rs rename to base_layer/core/src/transactions/transaction_components/kernel_features.rs diff --git a/base_layer/core/src/transactions/transaction/kernel_sum.rs b/base_layer/core/src/transactions/transaction_components/kernel_sum.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/kernel_sum.rs rename to base_layer/core/src/transactions/transaction_components/kernel_sum.rs diff --git a/base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs b/base_layer/core/src/transactions/transaction_components/mint_non_fungible_features.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs rename to base_layer/core/src/transactions/transaction_components/mint_non_fungible_features.rs diff --git a/base_layer/core/src/transactions/transaction/mod.rs b/base_layer/core/src/transactions/transaction_components/mod.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/mod.rs rename to base_layer/core/src/transactions/transaction_components/mod.rs index eb397b8e07..5f55585f9d 100644 --- a/base_layer/core/src/transactions/transaction/mod.rs +++ b/base_layer/core/src/transactions/transaction_components/mod.rs @@ -63,8 +63,6 @@ mod output_flags; mod rewind_result; mod side_chain_checkpoint_features; mod template_parameter; -// TODO: in future, this module can be renamed -#[allow(clippy::module_inception)] mod transaction; mod transaction_builder; mod transaction_input; diff --git a/base_layer/core/src/transactions/transaction/output_features.rs b/base_layer/core/src/transactions/transaction_components/output_features.rs similarity index 99% rename from base_layer/core/src/transactions/transaction/output_features.rs rename to base_layer/core/src/transactions/transaction_components/output_features.rs index 4547151fbb..647c9890de 100644 --- a/base_layer/core/src/transactions/transaction/output_features.rs +++ b/base_layer/core/src/transactions/transaction_components/output_features.rs @@ -35,7 +35,7 @@ use tari_utilities::ByteArray; use super::OutputFeaturesVersion; use crate::{ consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}, - transactions::transaction::{ + transactions::transaction_components::{ AssetOutputFeatures, MintNonFungibleFeatures, OutputFlags, diff --git a/base_layer/core/src/transactions/transaction/output_features_version.rs b/base_layer/core/src/transactions/transaction_components/output_features_version.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/output_features_version.rs rename to base_layer/core/src/transactions/transaction_components/output_features_version.rs diff --git a/base_layer/core/src/transactions/transaction/output_flags.rs b/base_layer/core/src/transactions/transaction_components/output_flags.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/output_flags.rs rename to base_layer/core/src/transactions/transaction_components/output_flags.rs diff --git a/base_layer/core/src/transactions/transaction/rewind_result.rs b/base_layer/core/src/transactions/transaction_components/rewind_result.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/rewind_result.rs rename to base_layer/core/src/transactions/transaction_components/rewind_result.rs diff --git a/base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs b/base_layer/core/src/transactions/transaction_components/side_chain_checkpoint_features.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs rename to base_layer/core/src/transactions/transaction_components/side_chain_checkpoint_features.rs diff --git a/base_layer/core/src/transactions/transaction/template_parameter.rs b/base_layer/core/src/transactions/transaction_components/template_parameter.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/template_parameter.rs rename to base_layer/core/src/transactions/transaction_components/template_parameter.rs diff --git a/base_layer/core/src/transactions/transaction/test.rs b/base_layer/core/src/transactions/transaction_components/test.rs similarity index 99% rename from base_layer/core/src/transactions/transaction/test.rs rename to base_layer/core/src/transactions/transaction_components/test.rs index 2c513bbab6..51828e0039 100644 --- a/base_layer/core/src/transactions/transaction/test.rs +++ b/base_layer/core/src/transactions/transaction_components/test.rs @@ -42,7 +42,7 @@ use crate::{ tari_amount::{uT, MicroTari, T}, test_helpers, test_helpers::{create_sender_transaction_protocol_with, create_unblinded_txos, TestParams, UtxoTestParams}, - transaction::OutputFeatures, + transaction_components::OutputFeatures, transaction_protocol::{RewindData, TransactionProtocolError}, CryptoFactories, }, diff --git a/base_layer/core/src/transactions/transaction/transaction.rs b/base_layer/core/src/transactions/transaction_components/transaction.rs similarity index 97% rename from base_layer/core/src/transactions/transaction/transaction.rs rename to base_layer/core/src/transactions/transaction_components/transaction.rs index 3f497a1e16..ec5b32bfc1 100644 --- a/base_layer/core/src/transactions/transaction/transaction.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction.rs @@ -36,7 +36,13 @@ use tari_crypto::tari_utilities::hex::Hex; use crate::transactions::{ aggregated_body::AggregateBody, tari_amount::{uT, MicroTari}, - transaction::{OutputFeatures, TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, + transaction_components::{ + OutputFeatures, + TransactionError, + TransactionInput, + TransactionKernel, + TransactionOutput, + }, weight::TransactionWeight, CryptoFactories, }; diff --git a/base_layer/core/src/transactions/transaction/transaction_builder.rs b/base_layer/core/src/transactions/transaction_components/transaction_builder.rs similarity index 97% rename from base_layer/core/src/transactions/transaction/transaction_builder.rs rename to base_layer/core/src/transactions/transaction_components/transaction_builder.rs index f334ef2934..28cbf05265 100644 --- a/base_layer/core/src/transactions/transaction/transaction_builder.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_builder.rs @@ -28,7 +28,7 @@ use tari_common_types::types::{BlindingFactor, HashOutput}; use crate::transactions::{ aggregated_body::AggregateBody, tari_amount::MicroTari, - transaction::{Transaction, TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, + transaction_components::{Transaction, TransactionError, TransactionInput, TransactionKernel, TransactionOutput}, CryptoFactories, }; diff --git a/base_layer/core/src/transactions/transaction/transaction_input.rs b/base_layer/core/src/transactions/transaction_components/transaction_input.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/transaction_input.rs rename to base_layer/core/src/transactions/transaction_components/transaction_input.rs index 2385110592..32ff1858c9 100644 --- a/base_layer/core/src/transactions/transaction/transaction_input.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_input.rs @@ -43,8 +43,13 @@ use crate::{ consensus::ConsensusEncoding, covenants::Covenant, transactions::{ - transaction, - transaction::{transaction_output::TransactionOutput, OutputFeatures, TransactionError, UnblindedOutput}, + transaction_components, + transaction_components::{ + transaction_output::TransactionOutput, + OutputFeatures, + TransactionError, + UnblindedOutput, + }, }, }; @@ -307,7 +312,7 @@ impl TransactionInput { ref features, ref covenant, .. - } => transaction::hash_output(version, features, commitment, script, covenant).to_vec(), + } => transaction_components::hash_output(version, features, commitment, script, covenant).to_vec(), } } diff --git a/base_layer/core/src/transactions/transaction/transaction_input_version.rs b/base_layer/core/src/transactions/transaction_components/transaction_input_version.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/transaction_input_version.rs rename to base_layer/core/src/transactions/transaction_components/transaction_input_version.rs diff --git a/base_layer/core/src/transactions/transaction/transaction_kernel.rs b/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/transaction_kernel.rs rename to base_layer/core/src/transactions/transaction_components/transaction_kernel.rs index 5ffe7bb9dd..0eea1ee5ff 100644 --- a/base_layer/core/src/transactions/transaction/transaction_kernel.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_kernel.rs @@ -39,7 +39,7 @@ use crate::{ consensus::ConsensusEncoding, transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, TransactionError}, + transaction_components::{KernelFeatures, TransactionError}, transaction_protocol::{build_challenge, TransactionMetadata}, }, }; diff --git a/base_layer/core/src/transactions/transaction/transaction_kernel_version.rs b/base_layer/core/src/transactions/transaction_components/transaction_kernel_version.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/transaction_kernel_version.rs rename to base_layer/core/src/transactions/transaction_components/transaction_kernel_version.rs diff --git a/base_layer/core/src/transactions/transaction/transaction_output.rs b/base_layer/core/src/transactions/transaction_components/transaction_output.rs similarity index 99% rename from base_layer/core/src/transactions/transaction/transaction_output.rs rename to base_layer/core/src/transactions/transaction_components/transaction_output.rs index 728a0d4e08..ba05a9c8d0 100644 --- a/base_layer/core/src/transactions/transaction/transaction_output.rs +++ b/base_layer/core/src/transactions/transaction_components/transaction_output.rs @@ -59,8 +59,8 @@ use crate::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction, - transaction::{ + transaction_components, + transaction_components::{ full_rewind_result::FullRewindResult, rewind_result::RewindResult, OutputFeatures, @@ -365,7 +365,7 @@ impl TransactionOutput { /// Implement the canonical hashing function for TransactionOutput for use in ordering. impl Hashable for TransactionOutput { fn hash(&self) -> Vec { - transaction::hash_output( + transaction_components::hash_output( self.version, &self.features, &self.commitment, diff --git a/base_layer/core/src/transactions/transaction/transaction_output_version.rs b/base_layer/core/src/transactions/transaction_components/transaction_output_version.rs similarity index 100% rename from base_layer/core/src/transactions/transaction/transaction_output_version.rs rename to base_layer/core/src/transactions/transaction_components/transaction_output_version.rs diff --git a/base_layer/core/src/transactions/transaction/unblinded_output.rs b/base_layer/core/src/transactions/transaction_components/unblinded_output.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/unblinded_output.rs rename to base_layer/core/src/transactions/transaction_components/unblinded_output.rs index 8887fb8b54..00d8f7f11c 100644 --- a/base_layer/core/src/transactions/transaction/unblinded_output.rs +++ b/base_layer/core/src/transactions/transaction_components/unblinded_output.rs @@ -46,8 +46,8 @@ use crate::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction, - transaction::{ + transaction_components, + transaction_components::{ transaction_input::{SpentOutput, TransactionInput}, transaction_output::TransactionOutput, OutputFeatures, @@ -61,6 +61,7 @@ use crate::{ /// An unblinded output is one where the value and spending key (blinding factor) are known. This can be used to /// build both inputs and outputs (every input comes from an output) // TODO: Try to get rid of 'Serialize' and 'Deserialize' traits here; see related comment at 'struct RawTransactionInfo' +// #LOGGED #[derive(Clone, Serialize, Deserialize)] pub struct UnblindedOutput { pub version: TransactionOutputVersion, @@ -265,7 +266,8 @@ impl UnblindedOutput { // Note: added to the struct to ensure consistency between `commitment`, `spending_key` and `value`. pub fn hash(&self, factories: &CryptoFactories) -> Vec { let commitment = factories.commitment.commit_value(&self.spending_key, self.value.into()); - transaction::hash_output(self.version, &self.features, &commitment, &self.script, &self.covenant).to_vec() + transaction_components::hash_output(self.version, &self.features, &commitment, &self.script, &self.covenant) + .to_vec() } } diff --git a/base_layer/core/src/transactions/transaction/unblinded_output_builder.rs b/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs similarity index 98% rename from base_layer/core/src/transactions/transaction/unblinded_output_builder.rs rename to base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs index dd59127206..8855cbd232 100644 --- a/base_layer/core/src/transactions/transaction/unblinded_output_builder.rs +++ b/base_layer/core/src/transactions/transaction_components/unblinded_output_builder.rs @@ -27,7 +27,7 @@ use crate::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::{OutputFeatures, TransactionError, TransactionOutput, UnblindedOutput}, + transaction_components::{OutputFeatures, TransactionError, TransactionOutput, UnblindedOutput}, }, }; diff --git a/base_layer/core/src/transactions/transaction_protocol/mod.rs b/base_layer/core/src/transactions/transaction_protocol/mod.rs index 285c7bd562..711f35d841 100644 --- a/base_layer/core/src/transactions/transaction_protocol/mod.rs +++ b/base_layer/core/src/transactions/transaction_protocol/mod.rs @@ -93,7 +93,7 @@ use tari_crypto::{ }; use thiserror::Error; -use crate::transactions::{tari_amount::*, transaction::TransactionError}; +use crate::transactions::{tari_amount::*, transaction_components::TransactionError}; pub mod proto; pub mod recipient; diff --git a/base_layer/core/src/transactions/transaction_protocol/proto/transaction_sender.proto b/base_layer/core/src/transactions/transaction_protocol/proto/transaction_sender.proto index dadeab0b33..af91e6fdec 100644 --- a/base_layer/core/src/transactions/transaction_protocol/proto/transaction_sender.proto +++ b/base_layer/core/src/transactions/transaction_protocol/proto/transaction_sender.proto @@ -37,9 +37,6 @@ message TransactionSenderMessage { oneof message { bool None = 1; SingleRoundSenderData single = 2; - - // TODO: Three round types - bool Multiple = 3; } } diff --git a/base_layer/core/src/transactions/transaction_protocol/recipient.rs b/base_layer/core/src/transactions/transaction_protocol/recipient.rs index 7aae24f19d..28ceac3f4a 100644 --- a/base_layer/core/src/transactions/transaction_protocol/recipient.rs +++ b/base_layer/core/src/transactions/transaction_protocol/recipient.rs @@ -30,7 +30,7 @@ use tari_common_types::{ use crate::transactions::{ crypto_factories::CryptoFactories, - transaction::TransactionOutput, + transaction_components::TransactionOutput, transaction_protocol::{ sender::{SingleRoundSenderData as SD, TransactionSenderMessage}, single_receiver::SingleReceiverTransactionProtocol, @@ -215,7 +215,7 @@ mod test { crypto_factories::CryptoFactories, tari_amount::*, test_helpers::TestParams, - transaction::OutputFeatures, + transaction_components::OutputFeatures, transaction_protocol::{ build_challenge, sender::{SingleRoundSenderData, TransactionSenderMessage}, diff --git a/base_layer/core/src/transactions/transaction_protocol/sender.rs b/base_layer/core/src/transactions/transaction_protocol/sender.rs index 8c231d5b79..ad9a768db4 100644 --- a/base_layer/core/src/transactions/transaction_protocol/sender.rs +++ b/base_layer/core/src/transactions/transaction_protocol/sender.rs @@ -42,7 +42,7 @@ use crate::{ crypto_factories::CryptoFactories, fee::Fee, tari_amount::*, - transaction::{ + transaction_components::{ KernelBuilder, KernelFeatures, OutputFeatures, @@ -69,7 +69,7 @@ use crate::{ /// This struct contains all the information that a transaction initiator (the sender) will manage throughout the /// Transaction construction process. // TODO: Investigate necessity to use the 'Serialize' and 'Deserialize' traits here; this could potentially leak -// TODO: information when least expected. +// TODO: information when least expected. #LOGGED #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub(super) struct RawTransactionInfo { pub num_recipients: usize, @@ -139,7 +139,6 @@ pub struct SingleRoundSenderData { pub enum TransactionSenderMessage { None, Single(Box), - // TODO: Three round types Multiple, } @@ -766,7 +765,7 @@ mod test { crypto_factories::CryptoFactories, tari_amount::*, test_helpers::{create_test_input, create_unblinded_output, TestParams}, - transaction::{KernelFeatures, OutputFeatures, TransactionError, TransactionOutput}, + transaction_components::{KernelFeatures, OutputFeatures, TransactionError, TransactionOutput}, transaction_protocol::{ sender::SenderTransactionProtocol, single_receiver::SingleReceiverTransactionProtocol, diff --git a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs index 19ffc76800..b471c9e2f6 100644 --- a/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs +++ b/base_layer/core/src/transactions/transaction_protocol/single_receiver.rs @@ -30,7 +30,7 @@ use tari_crypto::{ use crate::transactions::{ crypto_factories::CryptoFactories, - transaction::TransactionOutput, + transaction_components::TransactionOutput, transaction_protocol::{ build_challenge, recipient::RecipientSignedMessage as RD, @@ -143,7 +143,7 @@ mod test { use crate::transactions::{ crypto_factories::CryptoFactories, tari_amount::*, - transaction::OutputFeatures, + transaction_components::OutputFeatures, transaction_protocol::{ build_challenge, sender::SingleRoundSenderData, diff --git a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs index e01e580904..7a500c03d9 100644 --- a/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs +++ b/base_layer/core/src/transactions/transaction_protocol/transaction_initializer.rs @@ -47,7 +47,7 @@ use crate::{ crypto_factories::CryptoFactories, fee::Fee, tari_amount::*, - transaction::{ + transaction_components::{ OutputFeatures, TransactionInput, TransactionOutput, @@ -290,7 +290,7 @@ impl SenderTransactionInitializer { .map(|o| self.fee.weighting().round_up_metadata_size(o.metadata_byte_size())) .sum::(); - // TODO: implement iter for FixedSet to avoid the clone + // TODO: implement iter for FixedSet to avoid the clone #LOGGED size += self .recipient_scripts .clone() @@ -359,7 +359,6 @@ impl SenderTransactionInitializer { let change_amount = v.checked_sub(change_fee); let change_sender_offset_private_key = PrivateKey::random(&mut OsRng); self.change_sender_offset_private_key = Some(change_sender_offset_private_key.clone()); - // TODO: Add unique id if needed match change_amount { // You can't win. Just add the change to the fee (which is less than the cost of adding another // output and go without a change output @@ -526,7 +525,6 @@ impl SenderTransactionInitializer { // If rewind data is present we produce a rewindable output, else a standard output let change_output = if let Some(rewind_data) = self.rewind_data.as_ref() { - // TODO: Should proof be verified? match change_unblinded_output.as_rewindable_transaction_output(factories, rewind_data, None) { Ok(o) => o, Err(e) => { @@ -534,7 +532,6 @@ impl SenderTransactionInitializer { }, } } else { - // TODO: Should proof be verified? match change_unblinded_output.as_transaction_output(factories) { Ok(o) => o, Err(e) => { @@ -680,7 +677,7 @@ mod test { fee::Fee, tari_amount::*, test_helpers::{create_test_input, create_unblinded_output, TestParams, UtxoTestParams}, - transaction::{OutputFeatures, MAX_TRANSACTION_INPUTS}, + transaction_components::{OutputFeatures, MAX_TRANSACTION_INPUTS}, transaction_protocol::{ sender::SenderState, transaction_initializer::SenderTransactionInitializer, diff --git a/base_layer/core/src/validation/block_validators/async_validator.rs b/base_layer/core/src/validation/block_validators/async_validator.rs index bf1b55b376..6f2e461abb 100644 --- a/base_layer/core/src/validation/block_validators/async_validator.rs +++ b/base_layer/core/src/validation/block_validators/async_validator.rs @@ -37,7 +37,7 @@ use crate::{ iterators::NonOverlappingIntegerPairIter, transactions::{ aggregated_body::AggregateBody, - transaction::{ + transaction_components::{ KernelSum, OutputFlags, TransactionError, diff --git a/base_layer/core/src/validation/block_validators/test.rs b/base_layer/core/src/validation/block_validators/test.rs index e6c620e017..7d81691a41 100644 --- a/base_layer/core/src/validation/block_validators/test.rs +++ b/base_layer/core/src/validation/block_validators/test.rs @@ -37,7 +37,7 @@ use crate::{ aggregated_body::AggregateBody, tari_amount::T, test_helpers::{schema_to_transaction, TransactionSchema}, - transaction::{OutputFeatures, OutputFlags, TransactionError, UnblindedOutput}, + transaction_components::{OutputFeatures, OutputFlags, TransactionError, UnblindedOutput}, CoinbaseBuilder, CryptoFactories, }, diff --git a/base_layer/core/src/validation/error.rs b/base_layer/core/src/validation/error.rs index 5ef6fb3b93..44c7bdb1a5 100644 --- a/base_layer/core/src/validation/error.rs +++ b/base_layer/core/src/validation/error.rs @@ -29,7 +29,7 @@ use crate::{ chain_storage::ChainStorageError, covenants::CovenantError, proof_of_work::{monero_rx::MergeMineError, PowError}, - transactions::transaction::TransactionError, + transactions::transaction_components::TransactionError, }; #[derive(Debug, Error)] diff --git a/base_layer/core/src/validation/helpers.rs b/base_layer/core/src/validation/helpers.rs index 6e08945e33..98f040fa2e 100644 --- a/base_layer/core/src/validation/helpers.rs +++ b/base_layer/core/src/validation/helpers.rs @@ -50,7 +50,7 @@ use crate::{ transactions::{ aggregated_body::AggregateBody, tari_amount::MicroTari, - transaction::{ + transaction_components::{ KernelSum, OutputFlags, TransactionError, @@ -850,7 +850,7 @@ mod test { mod check_maturity { use super::*; - use crate::transactions::transaction::{OutputFeatures, TransactionInputVersion}; + use crate::transactions::transaction_components::{OutputFeatures, TransactionInputVersion}; #[test] fn it_checks_the_input_maturity() { diff --git a/base_layer/core/src/validation/mocks.rs b/base_layer/core/src/validation/mocks.rs index e30006c4c2..f6c07d5639 100644 --- a/base_layer/core/src/validation/mocks.rs +++ b/base_layer/core/src/validation/mocks.rs @@ -32,7 +32,7 @@ use crate::{ blocks::{Block, BlockHeader, ChainBlock}, chain_storage::BlockchainBackend, proof_of_work::{sha3_difficulty, AchievedTargetDifficulty, Difficulty, PowAlgorithm}, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, validation::{ error::ValidationError, BlockSyncBodyValidation, diff --git a/base_layer/core/src/validation/test.rs b/base_layer/core/src/validation/test.rs index 3bc8039b2c..fb46d855e4 100644 --- a/base_layer/core/src/validation/test.rs +++ b/base_layer/core/src/validation/test.rs @@ -37,7 +37,7 @@ use crate::{ transactions::{ tari_amount::{uT, MicroTari}, test_helpers::{create_random_signature_from_s_key, create_utxo}, - transaction::{KernelBuilder, KernelFeatures, OutputFeatures, TransactionKernel}, + transaction_components::{KernelBuilder, KernelFeatures, OutputFeatures, TransactionKernel}, CryptoFactories, }, validation::{header_iter::HeaderIter, ChainBalanceValidator, FinalHorizonStateValidation}, diff --git a/base_layer/core/src/validation/traits.rs b/base_layer/core/src/validation/traits.rs index 01f7b8183a..3e4fb97322 100644 --- a/base_layer/core/src/validation/traits.rs +++ b/base_layer/core/src/validation/traits.rs @@ -27,7 +27,7 @@ use crate::{ blocks::{Block, BlockHeader, ChainBlock}, chain_storage::BlockchainBackend, proof_of_work::AchievedTargetDifficulty, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, validation::{error::ValidationError, DifficultyCalculator}, }; diff --git a/base_layer/core/src/validation/transaction_validators.rs b/base_layer/core/src/validation/transaction_validators.rs index 1676ced387..7d3991fc12 100644 --- a/base_layer/core/src/validation/transaction_validators.rs +++ b/base_layer/core/src/validation/transaction_validators.rs @@ -26,7 +26,7 @@ use tari_utilities::hex::Hex; use crate::{ chain_storage::{BlockchainBackend, BlockchainDatabase, PrunedOutput}, transactions::{ - transaction::{SpentOutput, Transaction}, + transaction_components::{SpentOutput, Transaction}, CryptoFactories, }, validation::{ diff --git a/base_layer/core/tests/async_db.rs b/base_layer/core/tests/async_db.rs index a059fa4017..8d8008b063 100644 --- a/base_layer/core/tests/async_db.rs +++ b/base_layer/core/tests/async_db.rs @@ -36,7 +36,7 @@ use tari_core::{ transactions::{ tari_amount::T, test_helpers::schema_to_transaction, - transaction::{TransactionOutput, UnblindedOutput}, + transaction_components::{TransactionOutput, UnblindedOutput}, CryptoFactories, }, txn_schema, diff --git a/base_layer/core/tests/base_node_rpc.rs b/base_layer/core/tests/base_node_rpc.rs index 7e175f4df9..d82e64357d 100644 --- a/base_layer/core/tests/base_node_rpc.rs +++ b/base_layer/core/tests/base_node_rpc.rs @@ -50,7 +50,7 @@ use tari_core::{ transactions::{ tari_amount::{uT, T}, test_helpers::schema_to_transaction, - transaction::{TransactionOutput, UnblindedOutput}, + transaction_components::{TransactionOutput, UnblindedOutput}, CryptoFactories, }, txn_schema, diff --git a/base_layer/core/tests/block_validation.rs b/base_layer/core/tests/block_validation.rs index d56087ec64..5533b12f56 100644 --- a/base_layer/core/tests/block_validation.rs +++ b/base_layer/core/tests/block_validation.rs @@ -40,7 +40,7 @@ use tari_core::{ aggregated_body::AggregateBody, tari_amount::{uT, T}, test_helpers::{create_unblinded_output, schema_to_transaction, spend_utxos, TestParams, UtxoTestParams}, - transaction::OutputFeatures, + transaction_components::OutputFeatures, CryptoFactories, }, txn_schema, diff --git a/base_layer/core/tests/chain_storage_tests/chain_storage.rs b/base_layer/core/tests/chain_storage_tests/chain_storage.rs index 04b7ab2b38..1f68792a60 100644 --- a/base_layer/core/tests/chain_storage_tests/chain_storage.rs +++ b/base_layer/core/tests/chain_storage_tests/chain_storage.rs @@ -47,7 +47,7 @@ use tari_core::{ transactions::{ tari_amount::{uT, MicroTari, T}, test_helpers::{schema_to_transaction, spend_utxos}, - transaction::{OutputFeatures, OutputFlags}, + transaction_components::{OutputFeatures, OutputFlags}, CryptoFactories, }, tx, diff --git a/base_layer/core/tests/helpers/block_builders.rs b/base_layer/core/tests/helpers/block_builders.rs index 007c91d92c..3e26ec57c1 100644 --- a/base_layer/core/tests/helpers/block_builders.rs +++ b/base_layer/core/tests/helpers/block_builders.rs @@ -43,7 +43,7 @@ use tari_core::{ TestParams, TransactionSchema, }, - transaction::{ + transaction_components::{ KernelBuilder, KernelFeatures, OutputFeatures, diff --git a/base_layer/core/tests/helpers/database.rs b/base_layer/core/tests/helpers/database.rs index e1132445a3..c2af63e9ce 100644 --- a/base_layer/core/tests/helpers/database.rs +++ b/base_layer/core/tests/helpers/database.rs @@ -23,7 +23,7 @@ use tari_core::{ blocks::{Block, BlockHeader, NewBlockTemplate}, consensus::{emission::Emission, ConsensusManager}, - transactions::{tari_amount::MicroTari, transaction::Transaction, CryptoFactories}, + transactions::{tari_amount::MicroTari, transaction_components::Transaction, CryptoFactories}, }; use crate::helpers::block_builders::create_coinbase; diff --git a/base_layer/core/tests/helpers/sample_blockchains.rs b/base_layer/core/tests/helpers/sample_blockchains.rs index 10f0d7baae..d96525514b 100644 --- a/base_layer/core/tests/helpers/sample_blockchains.rs +++ b/base_layer/core/tests/helpers/sample_blockchains.rs @@ -29,7 +29,7 @@ use tari_core::{ test_helpers::blockchain::{create_store_with_consensus, TempDatabase}, transactions::{ tari_amount::{uT, T}, - transaction::UnblindedOutput, + transaction_components::UnblindedOutput, CryptoFactories, }, txn_schema, diff --git a/base_layer/core/tests/helpers/test_block_builder.rs b/base_layer/core/tests/helpers/test_block_builder.rs index 2d036d212e..c3f6457761 100644 --- a/base_layer/core/tests/helpers/test_block_builder.rs +++ b/base_layer/core/tests/helpers/test_block_builder.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // -use tari_core::transactions::transaction::Transaction; +use tari_core::transactions::transaction_components::Transaction; #[derive(Default)] pub struct TestBlockBuilder {} diff --git a/base_layer/core/tests/helpers/test_blockchain.rs b/base_layer/core/tests/helpers/test_blockchain.rs index 9b52cfadbf..3cae2087bf 100644 --- a/base_layer/core/tests/helpers/test_blockchain.rs +++ b/base_layer/core/tests/helpers/test_blockchain.rs @@ -31,7 +31,7 @@ use tari_core::{ chain_storage::{BlockAddResult, BlockchainDatabase, ChainStorageError}, consensus::ConsensusManager, test_helpers::blockchain::TempDatabase, - transactions::{transaction::UnblindedOutput, CryptoFactories}, + transactions::{transaction_components::UnblindedOutput, CryptoFactories}, }; use tari_crypto::tari_utilities::Hashable; diff --git a/base_layer/core/tests/mempool.rs b/base_layer/core/tests/mempool.rs index 51d9fddcf5..b6634719dc 100644 --- a/base_layer/core/tests/mempool.rs +++ b/base_layer/core/tests/mempool.rs @@ -51,7 +51,7 @@ use tari_core::{ fee::Fee, tari_amount::{uT, MicroTari, T}, test_helpers::{create_unblinded_output, schema_to_transaction, spend_utxos, TestParams}, - transaction::{KernelBuilder, OutputFeatures, OutputFlags, Transaction, TransactionOutput}, + transaction_components::{KernelBuilder, OutputFeatures, OutputFlags, Transaction, TransactionOutput}, transaction_protocol::{build_challenge, TransactionMetadata}, CryptoFactories, }, diff --git a/base_layer/core/tests/node_comms_interface.rs b/base_layer/core/tests/node_comms_interface.rs index 16e2153b31..aec0d13b8e 100644 --- a/base_layer/core/tests/node_comms_interface.rs +++ b/base_layer/core/tests/node_comms_interface.rs @@ -42,7 +42,7 @@ use tari_core::{ transactions::{ tari_amount::MicroTari, test_helpers::{create_utxo, spend_utxos}, - transaction::{OutputFeatures, TransactionOutput, UnblindedOutput}, + transaction_components::{OutputFeatures, TransactionOutput, UnblindedOutput}, CryptoFactories, }, txn_schema, diff --git a/base_layer/core/tests/node_service.rs b/base_layer/core/tests/node_service.rs index bd23b961c1..c38edf8fb5 100644 --- a/base_layer/core/tests/node_service.rs +++ b/base_layer/core/tests/node_service.rs @@ -43,7 +43,7 @@ use tari_core::{ transactions::{ tari_amount::{uT, T}, test_helpers::{schema_to_transaction, spend_utxos}, - transaction::OutputFeatures, + transaction_components::OutputFeatures, CryptoFactories, }, txn_schema, diff --git a/base_layer/key_manager/src/mnemonic.rs b/base_layer/key_manager/src/mnemonic.rs index 723cd6a8b8..a6577e87c4 100644 --- a/base_layer/key_manager/src/mnemonic.rs +++ b/base_layer/key_manager/src/mnemonic.rs @@ -34,6 +34,7 @@ use crate::{ /// The Mnemonic system simplifies the encoding and decoding of a secret key into and from a Mnemonic word sequence /// It can autodetect the language of the Mnemonic word sequence // TODO: Develop a language autodetection mechanism to distinguish between ChineseTraditional and ChineseSimplified +// #LOGGED #[derive(Clone, Debug, PartialEq, EnumString, Display, Copy)] pub enum MnemonicLanguage { diff --git a/base_layer/wallet/src/assets/asset_manager.rs b/base_layer/wallet/src/assets/asset_manager.rs index 711d15b6b6..57b5910e19 100644 --- a/base_layer/wallet/src/assets/asset_manager.rs +++ b/base_layer/wallet/src/assets/asset_manager.rs @@ -25,7 +25,7 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey}, }; -use tari_core::transactions::transaction::{OutputFeatures, OutputFlags, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{OutputFeatures, OutputFlags, TemplateParameter, Transaction}; use crate::{ assets::Asset, diff --git a/base_layer/wallet/src/assets/asset_manager_handle.rs b/base_layer/wallet/src/assets/asset_manager_handle.rs index 904a97b980..c59cbac178 100644 --- a/base_layer/wallet/src/assets/asset_manager_handle.rs +++ b/base_layer/wallet/src/assets/asset_manager_handle.rs @@ -24,7 +24,7 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey}, }; -use tari_core::transactions::transaction::{OutputFeatures, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{OutputFeatures, TemplateParameter, Transaction}; use tari_service_framework::{reply_channel::SenderService, Service}; use crate::{ diff --git a/base_layer/wallet/src/assets/infrastructure/mod.rs b/base_layer/wallet/src/assets/infrastructure/mod.rs index be43bfeab0..1ef017db39 100644 --- a/base_layer/wallet/src/assets/infrastructure/mod.rs +++ b/base_layer/wallet/src/assets/infrastructure/mod.rs @@ -26,7 +26,7 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, FixedHash, PublicKey}, }; -use tari_core::transactions::transaction::{OutputFeatures, TemplateParameter, Transaction}; +use tari_core::transactions::transaction_components::{OutputFeatures, TemplateParameter, Transaction}; use crate::assets::Asset; diff --git a/base_layer/wallet/src/base_node_service/mock_base_node_service.rs b/base_layer/wallet/src/base_node_service/mock_base_node_service.rs index 18eda7a77c..db8f9ae57a 100644 --- a/base_layer/wallet/src/base_node_service/mock_base_node_service.rs +++ b/base_layer/wallet/src/base_node_service/mock_base_node_service.rs @@ -32,7 +32,7 @@ use crate::base_node_service::{ service::BaseNodeState, }; -/// TODO Move this into the test support utilities when we remove the Test Harness feature from this crate +/// TODO Move this into the test support utilities when we remove the Test Harness feature from this crate #LOGGED pub struct MockBaseNodeService { request_stream: Option>>, pub base_node_peer: Option, diff --git a/base_layer/wallet/src/error.rs b/base_layer/wallet/src/error.rs index 6f8c8e82ab..fa89fabb71 100644 --- a/base_layer/wallet/src/error.rs +++ b/base_layer/wallet/src/error.rs @@ -31,7 +31,7 @@ use tari_comms::{ peer_manager::{node_id::NodeIdError, PeerManagerError}, }; use tari_comms_dht::store_forward::StoreAndForwardError; -use tari_core::transactions::transaction::TransactionError; +use tari_core::transactions::transaction_components::TransactionError; use tari_crypto::tari_utilities::{hex::HexError, ByteArrayError}; use tari_key_manager::error::KeyManagerError; use tari_p2p::{initialization::CommsInitializationError, services::liveness::error::LivenessError}; @@ -107,7 +107,6 @@ pub const LOG_TARGET: &str = "tari::application"; impl From for ExitError { fn from(err: WalletError) -> Self { - // TODO: Log that outside log::error!(target: LOG_TARGET, "{}", err); Self::new(ExitCode::WalletError, err) } diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 3e06f26137..3d88ca6ddb 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -25,7 +25,7 @@ use tari_common::exit_codes::{ExitCode, ExitError}; use tari_comms::{connectivity::ConnectivityError, peer_manager::node_id::NodeIdError, protocol::rpc::RpcError}; use tari_comms_dht::outbound::DhtOutboundError; use tari_core::transactions::{ - transaction::TransactionError, + transaction_components::TransactionError, transaction_protocol::TransactionProtocolError, CoinbaseBuildError, }; diff --git a/base_layer/wallet/src/output_manager_service/handle.rs b/base_layer/wallet/src/output_manager_service/handle.rs index 9bfd99a350..856e6d7b8d 100644 --- a/base_layer/wallet/src/output_manager_service/handle.rs +++ b/base_layer/wallet/src/output_manager_service/handle.rs @@ -31,7 +31,13 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::{OutputFeatures, Transaction, TransactionOutput, UnblindedOutput, UnblindedOutputBuilder}, + transaction_components::{ + OutputFeatures, + Transaction, + TransactionOutput, + UnblindedOutput, + UnblindedOutputBuilder, + }, transaction_protocol::{sender::TransactionSenderMessage, RewindData}, ReceiverTransactionProtocol, SenderTransactionProtocol, diff --git a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs index afa80139f6..e112fff806 100644 --- a/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs +++ b/base_layer/wallet/src/output_manager_service/recovery/standard_outputs_recoverer.rs @@ -29,12 +29,13 @@ use tari_common_types::{ types::{PrivateKey, PublicKey, RangeProof}, }; use tari_core::transactions::{ - transaction::{TransactionOutput, UnblindedOutput}, + transaction_components::{TransactionOutput, UnblindedOutput}, CryptoFactories, }; use tari_crypto::{ inputs, keys::{PublicKey as PublicKeyTrait, SecretKey}, + script, tari_utilities::hex::Hex, }; @@ -90,17 +91,15 @@ where TBackend: OutputManagerBackend + 'static &self.master_key_manager.rewind_data().rewind_blinding_key, ) .ok() - .map(|v| ( v, output ) ) + .map(|v| (v, output)) }) - //TODO: This needs some investigation. We assume Nop script here and recovery here might create an - //TODO: unspendable output if the script does not equal Nop. - .map( - |(rewind_result, output)| { - // Todo we need to look here that we might want to fail a specific output and not recover it as this - // will only work if the script is a Nop script. If this is not a Nop script the recovered input - // will not be spendable. - let script_key = PrivateKey::random(&mut OsRng); - (UnblindedOutput::new( + .filter_map(|(rewind_result, output)| { + if output.script != script!(Nop) { + return None; + } + let script_key = PrivateKey::random(&mut OsRng); + Some(( + UnblindedOutput::new( output.version, rewind_result.committed_value, rewind_result.blinding_factor, @@ -111,11 +110,11 @@ where TBackend: OutputManagerBackend + 'static output.sender_offset_public_key, output.metadata_signature, 0, - output.covenant + output.covenant, ), - output.proof) - }, - ) + output.proof, + )) + }) .collect(); let rewind_time = start.elapsed(); trace!( diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index cef6150185..828fefeebb 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -39,7 +39,7 @@ use tari_core::{ transactions::{ fee::Fee, tari_amount::MicroTari, - transaction::{ + transaction_components::{ KernelFeatures, OutputFeatures, Transaction, @@ -677,7 +677,7 @@ where spending_key.clone(), single_round_sender_data.features.clone(), single_round_sender_data.script.clone(), - // TODO: The input data should be variable; this will only work for a Nop script + // TODO: The input data should be variable; this will only work for a Nop script #LOGGED inputs!(PublicKey::from_secret_key(&script_private_key)), script_private_key, single_round_sender_data.sender_offset_public_key.clone(), @@ -811,7 +811,7 @@ where ) .await?; - // TODO: improve this logic + // TODO: improve this logic #LOGGED let output_features = match unique_id { Some(ref _unique_id) => match input_selection .utxos @@ -1999,7 +1999,6 @@ where } /// Different UTXO selection strategies for choosing which UTXO's are used to fulfill a transaction -/// TODO Investigate and implement more optimal strategies #[derive(Debug, PartialEq)] pub enum UTXOSelectionStrategy { // Start from the smallest UTXOs and work your way up until the amount is covered. Main benefit diff --git a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs index 5941773396..59539aa8dd 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/backend.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/backend.rs @@ -3,7 +3,7 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, PublicKey}, }; -use tari_core::transactions::transaction::{OutputFlags, TransactionOutput}; +use tari_core::transactions::transaction_components::{OutputFlags, TransactionOutput}; use crate::output_manager_service::{ error::OutputManagerStorageError, diff --git a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs index 6f06e10460..da14e6e2a3 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database/mod.rs @@ -35,7 +35,7 @@ use tari_common_types::{ }; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::{OutputFlags, TransactionOutput}, + transaction_components::{OutputFlags, TransactionOutput}, }; use tari_crypto::tari_utilities::hex::Hex; use tari_key_manager::cipher_seed::CipherSeed; diff --git a/base_layer/wallet/src/output_manager_service/storage/models.rs b/base_layer/wallet/src/output_manager_service/storage/models.rs index a013e9aa51..b36b04b4d0 100644 --- a/base_layer/wallet/src/output_manager_service/storage/models.rs +++ b/base_layer/wallet/src/output_manager_service/storage/models.rs @@ -23,7 +23,11 @@ use std::cmp::Ordering; use tari_common_types::types::{BlockHash, Commitment, HashOutput, PrivateKey, RangeProof}; -use tari_core::transactions::{transaction::UnblindedOutput, transaction_protocol::RewindData, CryptoFactories}; +use tari_core::transactions::{ + transaction_components::UnblindedOutput, + transaction_protocol::RewindData, + CryptoFactories, +}; use tari_crypto::script::{ExecutionStack, TariScript}; use tari_utilities::hash::Hashable; diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs index 17ea7d156a..99ddf1a350 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/mod.rs @@ -36,7 +36,7 @@ use tari_common_types::{ transaction::TxId, types::{Commitment, PrivateKey, PublicKey}, }; -use tari_core::transactions::transaction::{OutputFlags, TransactionOutput}; +use tari_core::transactions::transaction_components::{OutputFlags, TransactionOutput}; use tari_crypto::{ script::{ExecutionStack, TariScript}, tari_utilities::{ @@ -271,7 +271,7 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { // TODO: This is a problem because the keymanager state does not have an index // meaning that update round trips to the database can't be found again. // I would suggest changing this to a different pattern for retrieval, perhaps - // only returning the columns that are needed. + // only returning the columns that are needed. #LOGGED Some(DbValue::KeyManagerState(KeyManagerState::try_from(km)?)) }, } @@ -1675,7 +1675,7 @@ mod test { use tari_core::transactions::{ tari_amount::MicroTari, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, - transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + transaction_components::{OutputFeatures, TransactionInput, UnblindedOutput}, CryptoFactories, }; use tari_crypto::script; @@ -1749,16 +1749,36 @@ mod test { o.commit(&conn).unwrap(); } - // #todo: fix tests - // assert_eq!(OutputSql::index(&conn).unwrap(), outputs); - // assert_eq!( - // OutputSql::index_status(OutputStatus::Unspent, &conn).unwrap(), - // outputs_unspent - // ); - // assert_eq!( - // OutputSql::index_status(OutputStatus::Spent, &conn).unwrap(), - // outputs_spent - // ); + assert_eq!( + OutputSql::index(&conn) + .unwrap() + .iter() + .map(|o| o.spending_key.clone()) + .collect::>>(), + outputs.iter().map(|o| o.spending_key.clone()).collect::>>() + ); + assert_eq!( + OutputSql::index_status(OutputStatus::Unspent, &conn) + .unwrap() + .iter() + .map(|o| o.spending_key.clone()) + .collect::>>(), + outputs_unspent + .iter() + .map(|o| o.spending_key.clone()) + .collect::>>() + ); + assert_eq!( + OutputSql::index_status(OutputStatus::Spent, &conn) + .unwrap() + .iter() + .map(|o| o.spending_key.clone()) + .collect::>>(), + outputs_spent + .iter() + .map(|o| o.spending_key.clone()) + .collect::>>() + ); assert_eq!( OutputSql::find(&outputs[0].spending_key, &conn).unwrap().spending_key, diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index c33f29faa3..61e18b4231 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -33,7 +33,7 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::{OutputFeatures, OutputFlags, UnblindedOutput}, + transaction_components::{OutputFeatures, OutputFlags, UnblindedOutput}, CryptoFactories, }, }; @@ -451,7 +451,6 @@ impl OutputSql { Ok(()) } - // TODO: This method needs to be checked for concurrency pub fn update( &self, updated_output: UpdateOutput, diff --git a/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs index a30420fdef..40f0222df0 100644 --- a/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs +++ b/base_layer/wallet/src/output_manager_service/tasks/txo_validation_task.rs @@ -356,8 +356,6 @@ where Ok(last_mined_header_hash) } - // TODO: remove this duplicated code from transaction validation protocol - async fn get_base_node_block_at_height( &mut self, height: u64, diff --git a/base_layer/wallet/src/storage/sqlite_utilities/mod.rs b/base_layer/wallet/src/storage/sqlite_utilities/mod.rs index 7d766f9097..a70cae5e03 100644 --- a/base_layer/wallet/src/storage/sqlite_utilities/mod.rs +++ b/base_layer/wallet/src/storage/sqlite_utilities/mod.rs @@ -26,7 +26,6 @@ use std::{ time::Duration, }; -use diesel::{ExpressionMethods, QueryDsl, SqliteConnection}; use fs2::FileExt; use log::*; use tari_common_sqlite::sqlite_connection_pool::SqliteConnectionPool; @@ -65,8 +64,6 @@ pub fn run_migration_and_create_sqlite_connection>( pool.create_pool()?; let connection = pool.get_pooled_connection()?; - check_for_incompatible_db_encryption(&connection)?; - embed_migrations!("./migrations"); embedded_migrations::run(&connection) .map_err(|err| WalletStorageError::DatabaseMigrationError(format!("Database migration failed {}", err)))?; @@ -162,24 +159,3 @@ pub fn initialize_sqlite_database_backends( contacts_backend, )) } - -/// This method detects if the database contains the old incompatable encryption data and errors rather than breaking -/// the DB -/// TODO remove at next testnet reset -fn check_for_incompatible_db_encryption(connection: &SqliteConnection) -> Result<(), WalletStorageError> { - use crate::{diesel::RunQueryDsl, schema::wallet_settings, storage::sqlite_db::wallet::WalletSettingSql}; - - if wallet_settings::table - .filter(wallet_settings::key.eq("MasterSecretKey".to_string())) - .first::(connection) - .is_ok() - { - return Err(WalletStorageError::AeadError( - "This wallet database is incompatible with the new form of encryption. Halting to preserve this database \ - structure. Revert to a version of tari_console_wallet prior to 0.13.0" - .to_string(), - )); - } - - Ok(()) -} diff --git a/base_layer/wallet/src/tokens/token_manager.rs b/base_layer/wallet/src/tokens/token_manager.rs index 949f00b218..217ca38a63 100644 --- a/base_layer/wallet/src/tokens/token_manager.rs +++ b/base_layer/wallet/src/tokens/token_manager.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use log::*; -use tari_core::transactions::transaction::OutputFlags; +use tari_core::transactions::transaction_components::OutputFlags; use crate::{ error::WalletError, diff --git a/base_layer/wallet/src/transaction_service/error.rs b/base_layer/wallet/src/transaction_service/error.rs index 406457b0b2..662807fb93 100644 --- a/base_layer/wallet/src/transaction_service/error.rs +++ b/base_layer/wallet/src/transaction_service/error.rs @@ -26,7 +26,10 @@ use serde_json::Error as SerdeJsonError; use tari_common_types::transaction::{TransactionConversionError, TransactionDirectionError, TxId}; use tari_comms::{connectivity::ConnectivityError, peer_manager::node_id::NodeIdError, protocol::rpc::RpcError}; use tari_comms_dht::outbound::DhtOutboundError; -use tari_core::transactions::{transaction::TransactionError, transaction_protocol::TransactionProtocolError}; +use tari_core::transactions::{ + transaction_components::TransactionError, + transaction_protocol::TransactionProtocolError, +}; use tari_crypto::tari_utilities::ByteArrayError; use tari_p2p::services::liveness::error::LivenessError; use tari_service_framework::reply_channel::TransportChannelError; @@ -225,7 +228,7 @@ pub enum TransactionStorageError { /// include the ID of the protocol #[derive(Debug)] pub struct TransactionServiceProtocolError { - // TODO: Replace with T or something to account for OperationId or TxId + // TODO: Replace with T or something to account for OperationId or TxId #LOGGED pub id: u64, pub error: TransactionServiceError, } diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index d996d7cf74..2df0115ec4 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -30,7 +30,7 @@ use tari_common_types::{ use tari_comms::types::CommsPublicKey; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::{Transaction, TransactionOutput}, + transaction_components::{Transaction, TransactionOutput}, }; use tari_service_framework::reply_channel::SenderService; use tari_utilities::hex::Hex; @@ -225,7 +225,6 @@ pub enum TransactionEvent { is_valid: bool, }, TransactionMinedRequestTimedOut(TxId), - // TODO: Split into normal transaction mined and coinbase transaction mined TransactionMinedUnconfirmed { tx_id: TxId, num_confirmations: u64, diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs index f90ab24cfd..6674c0e8bf 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs @@ -37,7 +37,7 @@ use tari_core::{ proto::wallet_rpc::{TxLocation, TxQueryResponse, TxSubmissionRejectionReason, TxSubmissionResponse}, rpc::BaseNodeWalletRpcClient, }, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; use tari_crypto::tari_utilities::hex::Hex; use tokio::{sync::watch, time::sleep}; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index 3d5f8189b5..d3df9907e6 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -31,7 +31,7 @@ use tari_common_types::{ }; use tari_comms::types::CommsPublicKey; use tari_core::transactions::{ - transaction::Transaction, + transaction_components::Transaction, transaction_protocol::{recipient::RecipientState, sender::TransactionSenderMessage}, }; use tari_crypto::tari_utilities::Hashable; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index c9e92680ac..59819d5d85 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -38,7 +38,7 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::KernelFeatures, + transaction_components::KernelFeatures, transaction_protocol::{ proto::protocol as proto, recipient::RecipientSignedMessage, @@ -262,7 +262,6 @@ where tx_id, self.dest_pubkey.clone(), self.amount, - // TODO: put value in here fee, sender_protocol.clone(), TransactionStatus::Pending, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 4389a4c955..3793454b73 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -44,7 +44,7 @@ use tari_core::{ proto::base_node as base_node_proto, transactions::{ tari_amount::MicroTari, - transaction::{KernelFeatures, OutputFeatures, Transaction, TransactionOutput, UnblindedOutput}, + transaction_components::{KernelFeatures, OutputFeatures, Transaction, TransactionOutput, UnblindedOutput}, transaction_protocol::{ proto::protocol as proto, recipient::RecipientSignedMessage, @@ -335,7 +335,7 @@ where }, //Incoming request Some(request_context) = request_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (request, reply_tx) = request_context.split(); let event = format!("Handling Service API Request ({})", request); @@ -358,7 +358,7 @@ where }, // Incoming Transaction messages from the Comms layer Some(msg) = transaction_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); trace!(target: LOG_TARGET, "Handling Transaction Message, Trace: {}", msg.dht_header.message_tag); @@ -387,7 +387,7 @@ where }, // Incoming Transaction Reply messages from the Comms layer Some(msg) = transaction_reply_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); trace!(target: LOG_TARGET, "Handling Transaction Reply Message, Trace: {}", msg.dht_header.message_tag); @@ -417,7 +417,7 @@ where }, // Incoming Finalized Transaction messages from the Comms layer Some(msg) = transaction_finalized_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); trace!(target: LOG_TARGET, @@ -454,7 +454,7 @@ where }, // Incoming messages from the Comms layer Some(msg) = base_node_response_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); trace!(target: LOG_TARGET, "Handling Base Node Response, Trace: {}", msg.dht_header.message_tag); @@ -472,7 +472,7 @@ where } // Incoming messages from the Comms layer Some(msg) = transaction_cancelled_stream.next() => { - // TODO: Remove time measurements; this is to aid in system testing only + // TODO: Remove time measurements; this is to aid in system testing only #LOGGED let start = Instant::now(); let (origin_public_key, inner_msg) = msg.clone().into_origin_and_inner(); trace!(target: LOG_TARGET, "Handling Transaction Cancelled message, Trace: {}", msg.dht_header.message_tag); @@ -949,8 +949,6 @@ where let sender_offset_private_key = stp .get_recipient_sender_offset_private_key(0) .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; - // TODO: Add a standardized Diffie-Hellman method to the tari_crypto library that will return a private key, - // TODO: then come back and use it here. let spend_key = PrivateKey::from_bytes( CommsPublicKey::shared_secret(&sender_offset_private_key.clone(), &dest_pubkey.clone()).as_bytes(), ) @@ -1115,8 +1113,6 @@ where let sender_offset_private_key = stp .get_recipient_sender_offset_private_key(0) .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; - // TODO: Add a standardized Diffie-Hellman method to the tari_crypto library that will return a private key, - // TODO: then come back and use it here. let spend_key = PrivateKey::from_bytes( CommsPublicKey::shared_secret(&sender_offset_private_key.clone(), &dest_pubkey.clone()).as_bytes(), ) diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index d55a794272..c51bdaf963 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -36,7 +36,7 @@ use tari_common_types::{ types::{BlindingFactor, BlockHash}, }; use tari_comms::types::CommsPublicKey; -use tari_core::transactions::{tari_amount::MicroTari, transaction::Transaction}; +use tari_core::transactions::{tari_amount::MicroTari, transaction_components::Transaction}; use crate::transaction_service::{ error::TransactionStorageError, @@ -607,7 +607,6 @@ where T: TransactionBackend + 'static self.get_completed_transactions_by_cancelled(true).await } - // TODO: all the single getters should use an Option rather than an error to indicate not found. pub async fn get_any_transaction(&self, tx_id: TxId) -> Result, TransactionStorageError> { let db_clone = self.db.clone(); let key = DbKey::AnyTransaction(tx_id); diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs index 756ed3312b..62febbe9b0 100644 --- a/base_layer/wallet/src/transaction_service/storage/models.rs +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -29,7 +29,7 @@ use tari_common_types::{ use tari_comms::types::CommsPublicKey; use tari_core::transactions::{ tari_amount::MicroTari, - transaction::Transaction, + transaction_components::Transaction, ReceiverTransactionProtocol, SenderTransactionProtocol, }; diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index a3a6566a86..9076b1c259 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -2048,7 +2048,7 @@ impl UnconfirmedTransactionInfoSql { pub fn fetch_unconfirmed_transactions_info( conn: &SqliteConnection, ) -> Result, TransactionStorageError> { - // TODO: Should we not return cancelled transactions as well and handle it upstream? It could be mined. + // TODO: Should we not return cancelled transactions as well and handle it upstream? It could be mined. #LOGGED let query_result = completed_transactions::table .select(( completed_transactions::tx_id, @@ -2096,7 +2096,7 @@ mod test { transactions::{ tari_amount::MicroTari, test_helpers::{create_unblinded_output, TestParams}, - transaction::{OutputFeatures, Transaction}, + transaction_components::{OutputFeatures, Transaction}, transaction_protocol::sender::TransactionSenderMessage, CryptoFactories, ReceiverTransactionProtocol, diff --git a/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs index 8ae6a31592..8a648beba7 100644 --- a/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs +++ b/base_layer/wallet/src/transaction_service/tasks/send_finalized_transaction.rs @@ -30,7 +30,7 @@ use tari_comms_dht::{ domain_message::OutboundDomainMessage, outbound::{OutboundEncryption, OutboundMessageRequester, SendMessageResponse}, }; -use tari_core::transactions::{transaction::Transaction, transaction_protocol::proto}; +use tari_core::transactions::{transaction_components::Transaction, transaction_protocol::proto}; use tari_p2p::tari_message::TariMessageType; use crate::transaction_service::{ diff --git a/base_layer/wallet/src/utxo_scanner_service/service.rs b/base_layer/wallet/src/utxo_scanner_service/service.rs index 31af81c0c3..0f20d76fc7 100644 --- a/base_layer/wallet/src/utxo_scanner_service/service.rs +++ b/base_layer/wallet/src/utxo_scanner_service/service.rs @@ -52,7 +52,7 @@ pub const LOG_TARGET: &str = "wallet::utxo_scanning"; // Cache 1 days worth of headers. // TODO Determine a better strategy for maintaining a cache. Logarithmic sampling has been suggested but the problem // with it is that as you move on to the next block you need to resample say a 100 headers where a simple window like -// this only samples 1 header per new block. A ticket has been added to the backlog to think about this +// this only samples 1 header per new block. A ticket has been added to the backlog to think about this #LOGGED pub const SCANNED_BLOCK_CACHE_SIZE: u64 = 720; pub struct UtxoScannerService diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index 5eec755347..8aa78b087c 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -39,7 +39,7 @@ use tari_core::{ proto::base_node::SyncUtxosByBlockRequest, transactions::{ tari_amount::MicroTari, - transaction::{TransactionOutput, UnblindedOutput}, + transaction_components::{TransactionOutput, UnblindedOutput}, }, }; use tari_shutdown::ShutdownSignal; diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 47ec10a00e..446e4e4cb5 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -43,7 +43,7 @@ use tari_core::{ covenants::Covenant, transactions::{ tari_amount::MicroTari, - transaction::{OutputFeatures, UnblindedOutput}, + transaction_components::{OutputFeatures, UnblindedOutput}, CryptoFactories, }, }; diff --git a/base_layer/wallet/tests/output_manager_service_tests/service.rs b/base_layer/wallet/tests/output_manager_service_tests/service.rs index 10de433c8d..35ec1dc1c0 100644 --- a/base_layer/wallet/tests/output_manager_service_tests/service.rs +++ b/base_layer/wallet/tests/output_manager_service_tests/service.rs @@ -41,7 +41,7 @@ use tari_core::{ fee::Fee, tari_amount::{uT, MicroTari}, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, - transaction::{OutputFeatures, OutputFlags}, + transaction_components::{OutputFeatures, OutputFlags}, transaction_protocol::sender::TransactionSenderMessage, weight::TransactionWeight, CryptoFactories, diff --git a/base_layer/wallet/tests/support/comms_rpc.rs b/base_layer/wallet/tests/support/comms_rpc.rs index e1da122908..84f0041779 100644 --- a/base_layer/wallet/tests/support/comms_rpc.rs +++ b/base_layer/wallet/tests/support/comms_rpc.rs @@ -63,7 +63,7 @@ use tari_core::{ TransactionOutput as TransactionOutputProto, }, }, - transactions::transaction::{Transaction, TransactionOutput}, + transactions::transaction_components::{Transaction, TransactionOutput}, }; use tari_utilities::Hashable; use tokio::{sync::mpsc, time::sleep}; @@ -558,9 +558,6 @@ impl BaseNodeWalletService for BaseNodeWalletRpcMockService { &self, request: Request, ) -> Result, RpcStatus> { - // TODO: delay_lock is blocking any other RPC method from being called (as well as blocking an async task) - // until this method returns. - // Although this is sort of fine in tests it is probably unintentional let delay_lock = *acquire_lock!(self.state.response_delay); if let Some(delay) = delay_lock { sleep(delay).await; @@ -841,7 +838,7 @@ mod test { rpc::{BaseNodeWalletRpcClient, BaseNodeWalletRpcServer}, }, proto::base_node::{ChainMetadata, TipInfoResponse}, - transactions::transaction::Transaction, + transactions::transaction_components::Transaction, }; use tokio::time::Duration; diff --git a/base_layer/wallet/tests/support/utils.rs b/base_layer/wallet/tests/support/utils.rs index 30179dc8bf..20367c05ff 100644 --- a/base_layer/wallet/tests/support/utils.rs +++ b/base_layer/wallet/tests/support/utils.rs @@ -25,7 +25,7 @@ use tari_common_types::types::{CommitmentFactory, PrivateKey, PublicKey}; use tari_core::transactions::{ tari_amount::MicroTari, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, - transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, + transaction_components::{OutputFeatures, TransactionInput, UnblindedOutput}, }; use tari_crypto::{ keys::{PublicKey as PublicKeyTrait, SecretKey as SecretKeyTrait}, diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index b2632638d7..45f7add1ba 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -76,7 +76,7 @@ use tari_core::{ fee::Fee, tari_amount::*, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, - transaction::{KernelBuilder, KernelFeatures, OutputFeatures, Transaction}, + transaction_components::{KernelBuilder, KernelFeatures, OutputFeatures, Transaction}, transaction_protocol::{ proto::protocol as proto, recipient::RecipientSignedMessage, diff --git a/base_layer/wallet/tests/transaction_service_tests/storage.rs b/base_layer/wallet/tests/transaction_service_tests/storage.rs index b23b8876ab..5b07e1fd83 100644 --- a/base_layer/wallet/tests/transaction_service_tests/storage.rs +++ b/base_layer/wallet/tests/transaction_service_tests/storage.rs @@ -35,7 +35,7 @@ use tari_core::{ transactions::{ tari_amount::{uT, MicroTari}, test_helpers::{create_unblinded_output, TestParams}, - transaction::{OutputFeatures, Transaction}, + transaction_components::{OutputFeatures, Transaction}, transaction_protocol::sender::TransactionSenderMessage, CryptoFactories, ReceiverTransactionProtocol, diff --git a/base_layer/wallet/tests/utxo_scanner.rs b/base_layer/wallet/tests/utxo_scanner.rs index bfef914141..bca4d4f215 100644 --- a/base_layer/wallet/tests/utxo_scanner.rs +++ b/base_layer/wallet/tests/utxo_scanner.rs @@ -36,7 +36,7 @@ use tari_core::{ base_node::rpc::BaseNodeWalletRpcServer, blocks::BlockHeader, proto::base_node::{ChainMetadata, TipInfoResponse}, - transactions::{tari_amount::MicroTari, transaction::UnblindedOutput, CryptoFactories}, + transactions::{tari_amount::MicroTari, transaction_components::UnblindedOutput, CryptoFactories}, }; use tari_key_manager::cipher_seed::CipherSeed; use tari_service_framework::reply_channel; diff --git a/base_layer/wallet/tests/wallet.rs b/base_layer/wallet/tests/wallet.rs index 4c56387e0b..d0ff92ea36 100644 --- a/base_layer/wallet/tests/wallet.rs +++ b/base_layer/wallet/tests/wallet.rs @@ -62,7 +62,7 @@ use tari_core::{ transactions::{ tari_amount::{uT, MicroTari}, test_helpers::{create_unblinded_output, TestParams}, - transaction::OutputFeatures, + transaction_components::OutputFeatures, CryptoFactories, }, }; diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 6d38f44146..2e1271fbe0 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -37,7 +37,7 @@ mod test { use tari_comms_dht::event::DhtEvent; use tari_core::transactions::{ tari_amount::{uT, MicroTari}, - transaction::Transaction, + transaction_components::Transaction, ReceiverTransactionProtocol, SenderTransactionProtocol, }; diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index e957cfc596..1c3d1a92ae 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -122,7 +122,7 @@ use tari_comms::{ use tari_comms_dht::{store_forward::SafConfig, DbConnectionUrl, DhtConfig}; use tari_core::{ covenants::Covenant, - transactions::{tari_amount::MicroTari, transaction::OutputFeatures, CryptoFactories}, + transactions::{tari_amount::MicroTari, transaction_components::OutputFeatures, CryptoFactories}, }; use tari_crypto::{ inputs, @@ -184,10 +184,10 @@ const LOG_TARGET: &str = "wallet_ffi"; pub type TariTransportType = tari_p2p::transport::TransportType; pub type TariPublicKey = tari_common_types::types::PublicKey; pub type TariPrivateKey = tari_common_types::types::PrivateKey; -pub type TariOutputFeatures = tari_core::transactions::transaction::OutputFeatures; +pub type TariOutputFeatures = tari_core::transactions::transaction_components::OutputFeatures; pub type TariCommsConfig = tari_p2p::initialization::P2pConfig; pub type TariCommitmentSignature = tari_common_types::types::ComSignature; -pub type TariTransactionKernel = tari_core::transactions::transaction::TransactionKernel; +pub type TariTransactionKernel = tari_core::transactions::transaction_components::TransactionKernel; pub type TariCovenant = tari_core::covenants::Covenant; pub struct TariContacts(Vec); @@ -3025,7 +3025,7 @@ pub unsafe extern "C" fn comms_config_create( ..Default::default() }, // TODO: This should be set to false for non-test wallets. See the `allow_test_addresses` field - // docstring for more info. + // docstring for more info. #LOGGED allow_test_addresses: true, listener_liveness_allowlist_cidrs: Vec::new(), listener_liveness_max_sessions: 0, diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index cf66c19cbf..0627b77452 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -30,7 +30,6 @@ //! of Callbacks that the client must implement and provide to the Wallet module to receive asynchronous replies and //! updates. -// TODO: Improve documentation #ifndef wallet_ffi_h #define wallet_ffi_h diff --git a/comms/dht/src/dedup/dedup_cache.rs b/comms/dht/src/dedup/dedup_cache.rs index 7d6221eaf1..ef56369a25 100644 --- a/comms/dht/src/dedup/dedup_cache.rs +++ b/comms/dht/src/dedup/dedup_cache.rs @@ -133,8 +133,7 @@ impl DedupCacheDatabase { dedup_cache::last_hit_at.eq(Utc::now().naive_utc()), )) .execute(&conn)?; - // TODO: Diesel support for RETURNING statements would remove this query, but is not - // TODO: available for Diesel + SQLite yet + let hits = dedup_cache::table .select(dedup_cache::number_of_hits) .filter(dedup_cache::body_hash.eq(&body_hash)) diff --git a/comms/dht/src/proto/envelope.proto b/comms/dht/src/proto/envelope.proto index c1e6407d7d..7be368c9e4 100644 --- a/comms/dht/src/proto/envelope.proto +++ b/comms/dht/src/proto/envelope.proto @@ -42,7 +42,7 @@ message DhtHeader { DhtMessageType message_type = 8; uint32 flags = 10; // Message trace ID - // TODO: Remove for mainnet or when testing message traces is not required + // TODO: Remove for mainnet or when testing message traces is not required #LOGGED uint64 message_tag = 11; // Expiry timestamp for the message google.protobuf.Timestamp expires = 12; diff --git a/dan_layer/core/src/models/asset_definition.rs b/dan_layer/core/src/models/asset_definition.rs index c88cdb9810..46482ee4f8 100644 --- a/dan_layer/core/src/models/asset_definition.rs +++ b/dan_layer/core/src/models/asset_definition.rs @@ -24,7 +24,7 @@ use std::{fmt, marker::PhantomData}; use serde::{self, de, Deserialize, Deserializer, Serialize}; use tari_common_types::types::PublicKey; -use tari_core::transactions::transaction::TemplateParameter; +use tari_core::transactions::transaction_components::TemplateParameter; use tari_crypto::tari_utilities::hex::Hex; #[derive(Deserialize, Clone, Debug)] diff --git a/dan_layer/core/src/models/base_layer_output.rs b/dan_layer/core/src/models/base_layer_output.rs index 963b7b8a50..35a3fed8ce 100644 --- a/dan_layer/core/src/models/base_layer_output.rs +++ b/dan_layer/core/src/models/base_layer_output.rs @@ -22,7 +22,7 @@ //! A trait to allow abstraction from a specific base layer output use tari_common_types::types::PublicKey; -use tari_core::transactions::transaction::OutputFeatures; +use tari_core::transactions::transaction_components::OutputFeatures; pub struct BaseLayerOutput { pub features: OutputFeatures, diff --git a/dan_layer/core/src/services/asset_processor.rs b/dan_layer/core/src/services/asset_processor.rs index 8e1f66b42d..dac631f9da 100644 --- a/dan_layer/core/src/services/asset_processor.rs +++ b/dan_layer/core/src/services/asset_processor.rs @@ -22,7 +22,7 @@ use std::convert::TryInto; -use tari_core::transactions::transaction::TemplateParameter; +use tari_core::transactions::transaction_components::TemplateParameter; use crate::{ digital_assets_error::DigitalAssetError, diff --git a/dan_layer/core/src/services/mocks/mod.rs b/dan_layer/core/src/services/mocks/mod.rs index 50097ba479..c701602523 100644 --- a/dan_layer/core/src/services/mocks/mod.rs +++ b/dan_layer/core/src/services/mocks/mod.rs @@ -28,7 +28,7 @@ use std::{ use async_trait::async_trait; use tari_common_types::types::PublicKey; -use tari_core::transactions::transaction::TemplateParameter; +use tari_core::transactions::transaction_components::TemplateParameter; use super::CommitteeManager; use crate::{ diff --git a/dan_layer/core/src/services/payload_processor.rs b/dan_layer/core/src/services/payload_processor.rs index 1c3641b5ee..c2c79daeda 100644 --- a/dan_layer/core/src/services/payload_processor.rs +++ b/dan_layer/core/src/services/payload_processor.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use async_trait::async_trait; -use tari_core::transactions::transaction::TemplateParameter; +use tari_core::transactions::transaction_components::TemplateParameter; use crate::{ digital_assets_error::DigitalAssetError, diff --git a/dan_layer/core/src/templates/tip002_template.rs b/dan_layer/core/src/templates/tip002_template.rs index b20b951cf6..b21d535a21 100644 --- a/dan_layer/core/src/templates/tip002_template.rs +++ b/dan_layer/core/src/templates/tip002_template.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use prost::Message; -use tari_core::transactions::transaction::TemplateParameter; +use tari_core::transactions::transaction_components::TemplateParameter; use tari_crypto::tari_utilities::{hex::Hex, ByteArray}; use tari_dan_common_types::proto::tips::tip002; From 808e02292443b638e9c04a77e38e22b98f131e64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 08:24:35 +0000 Subject: [PATCH 09/28] chore(deps): bump follow-redirects from 1.14.4 to 1.14.8 in /integration_tests (#3829) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.4 to 1.14.8.
Commits
  • 3d81dc3 Release version 1.14.8 of the npm package.
  • 62e546a Drop confidential headers across schemes.
  • 2ede36d Release version 1.14.7 of the npm package.
  • 8b347cb Drop Cookie header across domains.
  • 6f5029a Release version 1.14.6 of the npm package.
  • af706be Ignore null headers.
  • d01ab7a Release version 1.14.5 of the npm package.
  • 40052ea Make compatible with Node 17.
  • 86f7572 Fix: clear internal timer on request abort to avoid leakage
  • 2e1eaf0 Keep Authorization header on subdomain redirects.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.14.4&new-version=1.14.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tari-project/tari/network/alerts).
--- integration_tests/package-lock.json | 70 ++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/integration_tests/package-lock.json b/integration_tests/package-lock.json index b0a9b0ba99..243dcff76c 100644 --- a/integration_tests/package-lock.json +++ b/integration_tests/package-lock.json @@ -1820,9 +1820,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.4", - "resolved": false, - "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "fs-constants": { "version": "1.0.0", @@ -3440,68 +3440,104 @@ "dependencies": { "@grpc/grpc-js": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.6.tgz", + "integrity": "sha512-v7+LQFbqZKmd/Tvf5/j1Xlbq6jXL/4d+gUtm2TNX4QiEC3ELWADmGr2dGlUyLl6aKTuYfsN72vAsO5zmavYkEg==", "requires": { "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.6.tgz", + "integrity": "sha512-DT14xgw3PSzPxwS13auTEwxhMMOoz33DPUKNtmYK/QYbBSpLXJy78FGGs5yVoxVobEqPm4iW9MOIoz0A3bLTRQ==", "requires": { "lodash.camelcase": "^4.3.0", "protobufjs": "^6.8.6" } }, "@protobufjs/aspromise": { - "version": "1.1.2" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" }, "@protobufjs/base64": { - "version": "1.1.2" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" }, "@protobufjs/codegen": { - "version": "2.0.4" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" }, "@protobufjs/eventemitter": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" }, "@protobufjs/fetch": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "@protobufjs/float": { - "version": "1.0.2" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" }, "@protobufjs/inquire": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" }, "@protobufjs/path": { - "version": "1.1.2" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" }, "@protobufjs/pool": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" }, "@protobufjs/utf8": { - "version": "1.1.0" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@types/long": { - "version": "4.0.1" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" }, "@types/node": { - "version": "16.3.2" + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.3.2.tgz", + "integrity": "sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==" }, "grpc-promise": { - "version": "1.4.0" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/grpc-promise/-/grpc-promise-1.4.0.tgz", + "integrity": "sha512-4BBXHXb5OjjBh7luylu8vFqL6H6aPn/LeqpQaSBeRzO/Xv95wHW/WkU9TJRqaCTMZ5wq9jTSvlJWp0vRJy1pVA==" }, "lodash.camelcase": { - "version": "4.3.0" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" }, "long": { - "version": "4.0.0" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, "protobufjs": { "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", From 69fbbbc94735fbb6bc2ae958ba9b834d23a8d95d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 09:45:42 +0000 Subject: [PATCH 10/28] chore(deps): bump follow-redirects from 1.14.7 to 1.14.8 in /applications/launchpad/gui-vue (#3831) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
Commits
  • 3d81dc3 Release version 1.14.8 of the npm package.
  • 62e546a Drop confidential headers across schemes.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.14.7&new-version=1.14.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tari-project/tari/network/alerts).
--- applications/launchpad/gui-vue/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/launchpad/gui-vue/package-lock.json b/applications/launchpad/gui-vue/package-lock.json index db72d6e596..96c92b69a7 100644 --- a/applications/launchpad/gui-vue/package-lock.json +++ b/applications/launchpad/gui-vue/package-lock.json @@ -9170,9 +9170,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "dev": true, "funding": [ { @@ -27170,9 +27170,9 @@ } }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "dev": true }, "for-in": { From 89f9e3e27d8e3ff72cac87cca8581d186b7cc105 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 11:00:42 +0000 Subject: [PATCH 11/28] chore(deps): bump follow-redirects from 1.14.5 to 1.14.8 in /applications/tari_web_extension (#3834) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.5 to 1.14.8.
Commits
  • 3d81dc3 Release version 1.14.8 of the npm package.
  • 62e546a Drop confidential headers across schemes.
  • 2ede36d Release version 1.14.7 of the npm package.
  • 8b347cb Drop Cookie header across domains.
  • 6f5029a Release version 1.14.6 of the npm package.
  • af706be Ignore null headers.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.14.5&new-version=1.14.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tari-project/tari/network/alerts).
--- applications/tari_web_extension/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/tari_web_extension/package-lock.json b/applications/tari_web_extension/package-lock.json index 6c2b8b03c5..50ad77409c 100644 --- a/applications/tari_web_extension/package-lock.json +++ b/applications/tari_web_extension/package-lock.json @@ -9162,9 +9162,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "funding": [ { "type": "individual", @@ -28314,9 +28314,9 @@ } }, "follow-redirects": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-in": { "version": "1.0.2", From 8fafad29c5671cd215cbc5fc333f74affb2a531f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 11:54:29 +0000 Subject: [PATCH 12/28] chore(deps): bump follow-redirects from 1.14.7 to 1.14.8 in /applications/tari_collectibles/web-app (#3833) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
Commits
  • 3d81dc3 Release version 1.14.8 of the npm package.
  • 62e546a Drop confidential headers across schemes.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.14.7&new-version=1.14.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tari-project/tari/network/alerts).
--- .../tari_collectibles/web-app/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/tari_collectibles/web-app/package-lock.json b/applications/tari_collectibles/web-app/package-lock.json index c5ff9373ed..f67cdad75c 100644 --- a/applications/tari_collectibles/web-app/package-lock.json +++ b/applications/tari_collectibles/web-app/package-lock.json @@ -9434,9 +9434,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "funding": [ { "type": "individual", @@ -28974,9 +28974,9 @@ } }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-in": { "version": "1.0.2", From c39520e858a3bdeae34050932db3a0192ce4a7b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 12:38:46 +0000 Subject: [PATCH 13/28] chore(deps): bump follow-redirects from 1.14.5 to 1.14.8 in /applications/tari_web_extension_example (#3832) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.5 to 1.14.8.
Commits
  • 3d81dc3 Release version 1.14.8 of the npm package.
  • 62e546a Drop confidential headers across schemes.
  • 2ede36d Release version 1.14.7 of the npm package.
  • 8b347cb Drop Cookie header across domains.
  • 6f5029a Release version 1.14.6 of the npm package.
  • af706be Ignore null headers.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.14.5&new-version=1.14.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) - `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language - `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language - `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language - `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tari-project/tari/network/alerts).
--- .../tari_web_extension_example/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/applications/tari_web_extension_example/package-lock.json b/applications/tari_web_extension_example/package-lock.json index 998a2a3633..8d5c15d1c4 100644 --- a/applications/tari_web_extension_example/package-lock.json +++ b/applications/tari_web_extension_example/package-lock.json @@ -9305,9 +9305,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", "funding": [ { "type": "individual", @@ -28838,9 +28838,9 @@ } }, "follow-redirects": { - "version": "1.14.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", - "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-in": { "version": "1.0.2", From 8791e93df52d05bec1a10d80e8c3a9416270d5d9 Mon Sep 17 00:00:00 2001 From: ZHANG Cheng Date: Mon, 14 Feb 2022 22:54:47 +0800 Subject: [PATCH 14/28] feat(wallet): add grpc method for setting base node (#3828) Description --- Aim to resolve #3821 Motivation and Context --- Learning via "good first issue" How Has This Been Tested? --- Code compiles, and manual test with `grpcurl`. --- applications/tari_app_grpc/proto/wallet.proto | 9 +++++++ .../src/grpc/wallet_grpc_server.rs | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index edb4a6957d..29c9f8b00b 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -77,6 +77,8 @@ service Wallet { rpc MintTokens(MintTokensRequest) returns (MintTokensResponse); rpc GetOwnedTokens(GetOwnedTokensRequest) returns (GetOwnedTokensResponse); + + rpc SetBaseNode(SetBaseNodeRequest) returns (SetBaseNodeResponse); } message GetVersionRequest { } @@ -335,3 +337,10 @@ message CancelTransactionResponse { message RevalidateRequest{} message RevalidateResponse{} + +message SetBaseNodeRequest { + string public_key_hex = 1; + string net_address = 2; +} + +message SetBaseNodeResponse{} diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index 68fe030250..b9aff22252 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -64,6 +64,8 @@ use tari_app_grpc::{ RevalidateResponse, SendShaAtomicSwapRequest, SendShaAtomicSwapResponse, + SetBaseNodeRequest, + SetBaseNodeResponse, TransactionDirection, TransactionInfo, TransactionStatus, @@ -76,7 +78,7 @@ use tari_common_types::{ array::copy_into_fixed_array, types::{BlockHash, PublicKey, Signature}, }; -use tari_comms::{types::CommsPublicKey, CommsNode}; +use tari_comms::{multiaddr::Multiaddr, types::CommsPublicKey, CommsNode}; use tari_core::transactions::{ tari_amount::MicroTari, transaction_components::{OutputFeatures, UnblindedOutput}, @@ -150,6 +152,29 @@ impl wallet_server::Wallet for WalletGrpcServer { })) } + async fn set_base_node( + &self, + request: Request, + ) -> Result, Status> { + let message = request.into_inner(); + let public_key = PublicKey::from_hex(&message.public_key_hex) + .map_err(|e| Status::invalid_argument(format!("Base node public key was not a valid pub key: {}", e)))?; + let net_address = message + .net_address + .parse::() + .map_err(|e| Status::invalid_argument(format!("Base node net address was not valid: {}", e)))?; + + println!("Setting base node peer..."); + println!("{}::{}", public_key, net_address); + let mut wallet = self.wallet.clone(); + wallet + .set_base_node_peer(public_key.clone(), net_address.clone()) + .await + .map_err(|e| Status::internal(format!("{:?}", e)))?; + + Ok(Response::new(SetBaseNodeResponse {})) + } + async fn get_balance(&self, _request: Request) -> Result, Status> { let mut output_service = self.get_output_manager_service(); let balance; From 00bc6e2bb1afbd709d3fc8492d182242e92c7620 Mon Sep 17 00:00:00 2001 From: ZHANG Cheng Date: Tue, 15 Feb 2022 00:36:03 +0800 Subject: [PATCH 15/28] feat: resize base node terminal on startup (#3827) feat(cli): resolves (#1728) Command Line Resize. Description --- Use [crossterm](https://github.com/crossterm-rs/crossterm) crate to resize cli properly. Motivation and Context --- My very first `good first issue` to work on tari codebase. ;-) How Has This Been Tested? --- Tested on macOS Monterey (12.1). Works for system Terminal app. Doesn't work for iTerm2. --- Cargo.lock | 26 ++++++++++++++++++++++++++ applications/tari_base_node/Cargo.toml | 1 + applications/tari_base_node/src/cli.rs | 19 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0bbc0b3c11..b129cb229a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,22 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "crossterm" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi 0.9.0", + "libc", + "mio 0.7.14", + "parking_lot 0.11.2", + "signal-hook 0.3.13", + "signal-hook-mio", + "winapi 0.3.9", +] + [[package]] name = "crossterm_winapi" version = "0.6.2" @@ -1501,6 +1517,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -6490,6 +6515,7 @@ dependencies = [ "bincode", "chrono", "config 0.9.3", + "crossterm 0.22.1", "either", "futures 0.3.21", "log", diff --git a/applications/tari_base_node/Cargo.toml b/applications/tari_base_node/Cargo.toml index e661d371ce..e66205b475 100644 --- a/applications/tari_base_node/Cargo.toml +++ b/applications/tari_base_node/Cargo.toml @@ -27,6 +27,7 @@ anyhow = "1.0.53" bincode = "1.3.1" chrono = { version = "0.4.19", default-features = false } config = { version = "0.9.3" } +crossterm = "0.22" either = "1.6.1" futures = { version = "^0.3.16", default-features = false, features = ["alloc"] } log = { version = "0.4.8", features = ["std"] } diff --git a/applications/tari_base_node/src/cli.rs b/applications/tari_base_node/src/cli.rs index 5731619602..da696c867c 100644 --- a/applications/tari_base_node/src/cli.rs +++ b/applications/tari_base_node/src/cli.rs @@ -20,7 +20,13 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::io::stdout; + use chrono::{Datelike, Utc}; +use crossterm::{ + execute, + terminal::{size, SetSize}, +}; use tari_app_utilities::consts; /// returns the top or bottom box line of the specified length @@ -100,6 +106,16 @@ fn multiline_find_display_length(lines: &str) -> usize { result } +/// Try to resize terminal to make sure the width is enough. +/// In case of error, just simply print out the error. +fn resize_terminal_to_fit_the_box(width: usize) { + if let Ok((_, rows)) = size() { + if let Err(e) = execute!(stdout(), SetSize(width as u16, rows)) { + println!("Can't resize terminal to fit the box. Error: {}", e) + } + } +} + /// Prints a pretty banner on the console as well as the list of available commands pub fn print_banner(commands: Vec, chunk_size: i32) { let chunks: Vec> = commands.chunks(chunk_size as usize).map(|x| x.to_vec()).collect(); @@ -141,6 +157,9 @@ pub fn print_banner(commands: Vec, chunk_size: i32) { let banner = include!("../assets/tari_banner.rs"); let target_line_length = multiline_find_display_length(banner); + + resize_terminal_to_fit_the_box(target_line_length); + for line in banner.lines() { println!("{}", line); } From 3403db6320210b2cd4497a87fad87d6d5dc87478 Mon Sep 17 00:00:00 2001 From: Hansie Odendaal <39146854+hansieodendaal@users.noreply.github.com> Date: Tue, 15 Feb 2022 09:02:38 +0200 Subject: [PATCH 16/28] feat: update console wallet tui (#3837) Description --- Updated console wallet TUI to better accommodate the expanding number of tabs: - Moved base node status to the bottom of the screen, expanding tab selection to the entire width. - Regained 1 line of real estate screen space at the bottom of the screen (with menu). - Refined some help text to fit on the screen. - Added a separate contacts tab to enable better handling of contacts not needing to access it via the transaction send tab. - Improved column spacing on the contacts tab/pane. - Improved the receive tab layout (moved connection details above the QR code). - Removed contacts editing ability from the sent tab; only selection remains. _**Some screenshots:**_ ![image](https://user-images.githubusercontent.com/39146854/153877245-dd354e74-f75c-46b4-bb82-4bfeac0fe38b.png) ![image](https://user-images.githubusercontent.com/39146854/153877820-28da3efa-563d-47ab-9fec-dd23c482d337.png) ![image](https://user-images.githubusercontent.com/39146854/153877839-859616dc-8dea-4e6b-b072-dc8f15634141.png) Motivation and Context --- See above How Has This Been Tested? --- System level tests --- .../tari_console_wallet/src/ui/app.rs | 23 +- .../src/ui/components/base_node.rs | 74 +++- .../src/ui/components/contacts_tab.rs | 371 ++++++++++++++++++ .../src/ui/components/mod.rs | 1 + .../src/ui/components/network_tab.rs | 2 +- .../src/ui/components/receive_tab.rs | 120 +++--- .../src/ui/components/send_tab.rs | 245 +----------- .../src/ui/components/transactions_tab.rs | 12 +- base_layer/common_types/src/transaction.rs | 16 +- 9 files changed, 537 insertions(+), 327 deletions(-) create mode 100644 applications/tari_console_wallet/src/ui/components/contacts_tab.rs diff --git a/applications/tari_console_wallet/src/ui/app.rs b/applications/tari_console_wallet/src/ui/app.rs index 145ce992e4..a67078909b 100644 --- a/applications/tari_console_wallet/src/ui/app.rs +++ b/applications/tari_console_wallet/src/ui/app.rs @@ -36,6 +36,7 @@ use crate::{ components::{ assets_tab::AssetsTab, base_node::BaseNode, + contacts_tab::ContactsTab, events_component::EventsComponent, log_tab::LogTab, menu::Menu, @@ -91,6 +92,7 @@ impl App { .add("Transactions".into(), Box::new(TransactionsTab::new())) .add("Send".into(), Box::new(SendTab::new(&app_state))) .add("Receive".into(), Box::new(ReceiveTab::new())) + .add("Contacts".into(), Box::new(ContactsTab::new())) .add("Network".into(), Box::new(NetworkTab::new(base_node_selected))) .add("Assets".into(), Box::new(AssetsTab::new())) .add("Tokens".into(), Box::new(TokensComponent::new())) @@ -173,17 +175,20 @@ impl App { .constraints([Constraint::Length(MAX_WIDTH), Constraint::Min(0)].as_ref()) .split(f.size()); let title_chunks = Layout::default() - .constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(2)].as_ref()) + .constraints( + [ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(2), + Constraint::Length(1), + ] + .as_ref(), + ) .split(max_width_layout[0]); - let title_halves = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(65), Constraint::Percentage(35)].as_ref()) - .split(title_chunks[0]); - - self.tabs.draw_titles(f, title_halves[0], &self.app_state); - self.base_node_status.draw(f, title_halves[1], &self.app_state); + self.tabs.draw_titles(f, title_chunks[0], &self.app_state); self.tabs.draw_content(f, title_chunks[1], &mut self.app_state); - self.menu.draw(f, title_chunks[2], &self.app_state); + self.base_node_status.draw(f, title_chunks[2], &self.app_state); + self.menu.draw(f, title_chunks[3], &self.app_state); } } diff --git a/applications/tari_console_wallet/src/ui/components/base_node.rs b/applications/tari_console_wallet/src/ui/components/base_node.rs index bfe2bd1c79..118f706455 100644 --- a/applications/tari_console_wallet/src/ui/components/base_node.rs +++ b/applications/tari_console_wallet/src/ui/components/base_node.rs @@ -23,14 +23,14 @@ use tari_wallet::connectivity_service::{OnlineStatus, WalletConnectivityInterface}; use tui::{ backend::Backend, - layout::Rect, - style::{Color, Modifier, Style}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, text::{Span, Spans}, widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::ui::{components::Component, state::AppState}; +use crate::ui::{components::Component, state::AppState, MAX_WIDTH}; pub struct BaseNode {} @@ -43,9 +43,13 @@ impl BaseNode { impl Component for BaseNode { fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) where B: Backend { - let base_node_state = app_state.get_base_node_state(); - let current_online_status = app_state.get_wallet_connectivity().get_connectivity_status(); + let title = Spans::from(vec![Span::styled( + " Base Node Status - ", + Style::default().fg(Color::White), + )]); + let current_online_status = app_state.get_wallet_connectivity().get_connectivity_status(); + let mut base_node_id_color = Color::White; let chain_info = match current_online_status { OnlineStatus::Connecting => Spans::from(vec![ Span::styled("Chain Tip:", Style::default().fg(Color::Magenta)), @@ -58,14 +62,27 @@ impl Component for BaseNode { Span::styled("Offline", Style::default().fg(Color::Red)), ]), OnlineStatus::Online => { + let base_node_state = app_state.get_base_node_state(); if let Some(ref metadata) = base_node_state.chain_metadata { let tip = metadata.height_of_longest_chain(); let synced = base_node_state.is_synced.unwrap_or_default(); let (tip_color, sync_text) = if synced { - (Color::Green, "Synced.") + ( + { + base_node_id_color = Color::Green; + base_node_id_color + }, + "Synced.", + ) } else { - (Color::Yellow, "Syncing...") + ( + { + base_node_id_color = Color::Yellow; + base_node_id_color + }, + "Syncing...", + ) }; let latency = base_node_state.latency.unwrap_or_default().as_millis(); @@ -100,11 +117,42 @@ impl Component for BaseNode { }, }; - let chain_metadata_paragraph = - Paragraph::new(chain_info).block(Block::default().borders(Borders::ALL).title(Span::styled( - "Base Node Status:", - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), - ))); - f.render_widget(chain_metadata_paragraph, area); + let base_node_id = Spans::from(vec![ + Span::styled(" Connected Base Node ID: ", Style::default().fg(Color::Magenta)), + Span::styled( + format!("{}", app_state.get_selected_base_node().node_id.clone()), + Style::default().fg(base_node_id_color), + ), + Span::styled(" ", Style::default().fg(Color::White)), + ]); + + let chunks = Layout::default() + .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) + .split(area); + + let columns = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Ratio(title.width() as u32, MAX_WIDTH as u32), + Constraint::Ratio( + MAX_WIDTH.saturating_sub((title.width() + base_node_id.width()) as u16) as u32, + MAX_WIDTH as u32, + ), + Constraint::Ratio(base_node_id.width() as u32, MAX_WIDTH as u32), + ] + .as_ref(), + ) + .split(chunks[0]); + + let paragraph = Paragraph::new(title).block(Block::default()); + f.render_widget(paragraph, columns[0]); + let paragraph = Paragraph::new(chain_info).block(Block::default()); + f.render_widget(paragraph, columns[1]); + let paragraph = Paragraph::new(base_node_id).block(Block::default()); + f.render_widget(paragraph, columns[2]); + + let divider = Block::default().borders(Borders::BOTTOM); + f.render_widget(divider, chunks[1]); } } diff --git a/applications/tari_console_wallet/src/ui/components/contacts_tab.rs b/applications/tari_console_wallet/src/ui/components/contacts_tab.rs new file mode 100644 index 0000000000..169388104c --- /dev/null +++ b/applications/tari_console_wallet/src/ui/components/contacts_tab.rs @@ -0,0 +1,371 @@ +use tokio::runtime::Handle; +use tui::{ + backend::Backend, + layout::{Constraint, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Clear, ListItem, Paragraph, Wrap}, + Frame, +}; +use unicode_width::UnicodeWidthStr; + +use crate::{ + ui::{ + components::{Component, KeyHandled}, + state::AppState, + widgets::{centered_rect_absolute, draw_dialog, MultiColumnList, WindowedListState}, + MAX_WIDTH, + }, + utils::formatting::display_compressed_string, +}; + +pub struct ContactsTab { + edit_contact_mode: ContactInputMode, + show_edit_contact: bool, + alias_field: String, + public_key_field: String, + error_message: Option, + contacts_list_state: WindowedListState, + confirmation_dialog: Option, +} + +impl ContactsTab { + pub fn new() -> Self { + Self { + edit_contact_mode: ContactInputMode::None, + show_edit_contact: false, + alias_field: String::new(), + public_key_field: String::new(), + error_message: None, + contacts_list_state: WindowedListState::new(), + confirmation_dialog: None, + } + } + + fn draw_contacts(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) + where B: Backend { + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Contacts", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )); + f.render_widget(block, area); + let list_areas = Layout::default() + .constraints([Constraint::Length(1), Constraint::Min(42)].as_ref()) + .margin(1) + .split(area); + + let instructions = Paragraph::new(Spans::from(vec![ + Span::raw("Use "), + Span::styled("Up↑/Down↓ Keys", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to select a contact, "), + Span::styled("E", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" or "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to (e)dit a contact, "), + Span::styled("D", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to (d)elete a contact and "), + Span::styled("N", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to create a (n)ew contact."), + ])) + .wrap(Wrap { trim: true }); + f.render_widget(instructions, list_areas[0]); + self.contacts_list_state.set_num_items(app_state.get_contacts().len()); + let mut list_state = self + .contacts_list_state + .get_list_state((list_areas[1].height as usize).saturating_sub(3)); + let window = self.contacts_list_state.get_start_end(); + let windowed_view = app_state.get_contacts_slice(window.0, window.1); + + let mut column0_items = Vec::new(); + let mut column1_items = Vec::new(); + let mut column2_items = Vec::new(); + for c in windowed_view.iter() { + column0_items.push(ListItem::new(Span::raw(c.alias.clone()))); + column1_items.push(ListItem::new(Span::raw(c.public_key.to_string()))); + column2_items.push(ListItem::new(Span::raw(display_compressed_string( + c.emoji_id.clone(), + 3, + 3, + )))); + } + let column_list = MultiColumnList::new() + .highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Magenta)) + .heading_style(Style::default().fg(Color::Magenta)) + .max_width(MAX_WIDTH) + .add_column(Some("Alias"), Some(25), column0_items) + .add_column(None, Some(2), Vec::new()) + .add_column(Some("Public Key"), Some(64), column1_items) + .add_column(None, Some(2), Vec::new()) + .add_column(Some("Emoji ID"), None, column2_items); + column_list.render(f, list_areas[1], &mut list_state); + } + + fn draw_edit_contact(&mut self, f: &mut Frame, area: Rect, _app_state: &AppState) + where B: Backend { + let popup_area = centered_rect_absolute(120, 10, area); + + f.render_widget(Clear, popup_area); + + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Add/Edit Contact", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )); + f.render_widget(block, popup_area); + let vert_chunks = Layout::default() + .constraints([Constraint::Length(2), Constraint::Length(3), Constraint::Length(3)].as_ref()) + .margin(1) + .split(popup_area); + + let instructions = Paragraph::new(Spans::from(vec![ + Span::raw("Press "), + Span::styled("L", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Alias", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("K", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to edit "), + Span::styled("Public Key/Emoji ID", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" field, "), + Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to save Contact."), + ])) + .block(Block::default()); + f.render_widget(instructions, vert_chunks[0]); + + let alias_input = Paragraph::new(self.alias_field.as_ref()) + .style(match self.edit_contact_mode { + ContactInputMode::Alias => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("A(l)ias:")); + f.render_widget(alias_input, vert_chunks[1]); + + let pubkey_input = Paragraph::new(self.public_key_field.as_ref()) + .style(match self.edit_contact_mode { + ContactInputMode::PubkeyEmojiId => Style::default().fg(Color::Magenta), + _ => Style::default(), + }) + .block(Block::default().borders(Borders::ALL).title("Public (K)ey / Emoji Id:")); + f.render_widget(pubkey_input, vert_chunks[2]); + + match self.edit_contact_mode { + ContactInputMode::None => (), + ContactInputMode::Alias => f.set_cursor( + // Put cursor past the end of the input text + vert_chunks[1].x + self.alias_field.width() as u16 + 1, + // Move one line down, from the border to the input line + vert_chunks[1].y + 1, + ), + ContactInputMode::PubkeyEmojiId => f.set_cursor( + // Put cursor past the end of the input text + vert_chunks[2].x + self.public_key_field.width() as u16 + 1, + // Move one line down, from the border to the input line + vert_chunks[2].y + 1, + ), + } + } + + fn on_key_confirmation_dialog(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { + if self.confirmation_dialog.is_some() { + if 'n' == c { + self.confirmation_dialog = None; + return KeyHandled::Handled; + } else if 'y' == c { + match self.confirmation_dialog { + None => (), + Some(ConfirmationDialogType::DeleteContact) => { + if 'y' == c { + if let Some(c) = self + .contacts_list_state + .selected() + .and_then(|i| app_state.get_contact(i)) + .cloned() + { + if let Err(_e) = Handle::current().block_on(app_state.delete_contact(c.public_key)) { + self.error_message = + Some("Could not delete selected contact\nPress Enter to continue.".to_string()); + } + } + self.confirmation_dialog = None; + return KeyHandled::Handled; + } + }, + } + } + } + + KeyHandled::NotHandled + } + + fn on_key_edit_contact(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { + if self.show_edit_contact && self.edit_contact_mode != ContactInputMode::None { + match self.edit_contact_mode { + ContactInputMode::None => return KeyHandled::Handled, + ContactInputMode::Alias => match c { + '\n' | '\t' => { + self.edit_contact_mode = ContactInputMode::PubkeyEmojiId; + return KeyHandled::Handled; + }, + c => { + self.alias_field.push(c); + return KeyHandled::Handled; + }, + }, + ContactInputMode::PubkeyEmojiId => match c { + '\n' => { + self.edit_contact_mode = ContactInputMode::None; + self.show_edit_contact = false; + + if let Err(_e) = Handle::current() + .block_on(app_state.upsert_contact(self.alias_field.clone(), self.public_key_field.clone())) + { + self.error_message = + Some("Invalid Public key or Emoji ID provided\n Press Enter to continue.".to_string()); + } + + self.alias_field = "".to_string(); + self.public_key_field = "".to_string(); + return KeyHandled::Handled; + }, + c => { + self.public_key_field.push(c); + return KeyHandled::Handled; + }, + }, + } + } + + KeyHandled::NotHandled + } + + fn on_key_show_contacts(&mut self, c: char, _app_state: &mut AppState) -> KeyHandled { + match c { + 'd' => { + if self.contacts_list_state.selected().is_none() { + return KeyHandled::NotHandled; + } + self.confirmation_dialog = Some(ConfirmationDialogType::DeleteContact); + return KeyHandled::Handled; + }, + 'n' => { + self.show_edit_contact = true; + self.edit_contact_mode = ContactInputMode::Alias; + return KeyHandled::Handled; + }, + _ => (), + } + + KeyHandled::NotHandled + } +} + +impl Component for ContactsTab { + fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) { + self.draw_contacts(f, area, app_state); + if self.show_edit_contact { + self.draw_edit_contact(f, area, app_state); + } + + match self.confirmation_dialog { + None => (), + Some(ConfirmationDialogType::DeleteContact) => { + draw_dialog( + f, + area, + "Confirm Delete".to_string(), + "Are you sure you want to delete this contact?\n(Y)es / (N)o".to_string(), + Color::Red, + 120, + 9, + ); + }, + } + } + + fn on_key(&mut self, app_state: &mut AppState, c: char) { + if self.error_message.is_some() { + if '\n' == c { + self.error_message = None; + } + return; + } + + if self.on_key_confirmation_dialog(c, app_state) == KeyHandled::Handled { + return; + } + + if self.on_key_edit_contact(c, app_state) == KeyHandled::Handled { + return; + } + + if self.on_key_show_contacts(c, app_state) == KeyHandled::Handled { + return; + } + + match c { + 'e' | '\n' => { + if let Some(c) = self + .contacts_list_state + .selected() + .and_then(|i| app_state.get_contact(i)) + { + self.public_key_field = c.public_key.clone(); + self.alias_field = c.alias.clone(); + self.show_edit_contact = true; + self.edit_contact_mode = ContactInputMode::Alias; + } + }, + _ => { + self.show_edit_contact = false; + self.edit_contact_mode = ContactInputMode::Alias; + self.public_key_field = "".to_string(); + }, + } + } + + fn on_up(&mut self, app_state: &mut AppState) { + self.contacts_list_state.set_num_items(app_state.get_contacts().len()); + self.contacts_list_state.previous(); + } + + fn on_down(&mut self, app_state: &mut AppState) { + self.contacts_list_state.set_num_items(app_state.get_contacts().len()); + self.contacts_list_state.next(); + } + + fn on_esc(&mut self, _: &mut AppState) { + if self.confirmation_dialog.is_some() { + return; + } + self.edit_contact_mode = ContactInputMode::None; + if self.show_edit_contact { + self.show_edit_contact = false; + } else { + self.contacts_list_state.select(None); + } + } + + fn on_backspace(&mut self, _app_state: &mut AppState) { + match self.edit_contact_mode { + ContactInputMode::Alias => { + let _ = self.alias_field.pop(); + }, + ContactInputMode::PubkeyEmojiId => { + let _ = self.public_key_field.pop(); + }, + ContactInputMode::None => {}, + } + } +} + +#[derive(PartialEq, Debug)] +pub enum ContactInputMode { + None, + Alias, + PubkeyEmojiId, +} + +#[derive(PartialEq, Debug)] +pub enum ConfirmationDialogType { + DeleteContact, +} diff --git a/applications/tari_console_wallet/src/ui/components/mod.rs b/applications/tari_console_wallet/src/ui/components/mod.rs index 80b299bdc5..43d129edcd 100644 --- a/applications/tari_console_wallet/src/ui/components/mod.rs +++ b/applications/tari_console_wallet/src/ui/components/mod.rs @@ -35,6 +35,7 @@ pub mod tabs_container; pub mod tokens_component; pub mod transactions_tab; pub use self::component::*; +pub mod contacts_tab; pub mod events_component; #[derive(PartialEq, Eq)] diff --git a/applications/tari_console_wallet/src/ui/components/network_tab.rs b/applications/tari_console_wallet/src/ui/components/network_tab.rs index 146262c117..0ac5233d27 100644 --- a/applications/tari_console_wallet/src/ui/components/network_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/network_tab.rs @@ -74,7 +74,7 @@ impl NetworkTab { Span::raw("Press "), Span::styled("B", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" and use "), - Span::styled("Up/Down Arrow Keys", Style::default().add_modifier(Modifier::BOLD)), + Span::styled("Up↑/Down↓ Keys", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to select a new Base Node, "), Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to set."), diff --git a/applications/tari_console_wallet/src/ui/components/receive_tab.rs b/applications/tari_console_wallet/src/ui/components/receive_tab.rs index aefe4dfc0b..0e46ac1844 100644 --- a/applications/tari_console_wallet/src/ui/components/receive_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/receive_tab.rs @@ -2,7 +2,7 @@ use tui::{ backend::Backend, layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, - text::Span, + text::{Span, Spans}, widgets::{Block, Borders, Paragraph}, Frame, }; @@ -24,84 +24,84 @@ impl ReceiveTab { )); f.render_widget(block, area); - let help_body_area = Layout::default() - .constraints([Constraint::Min(42)].as_ref()) - .margin(1) - .split(area); - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Length(48), Constraint::Min(1)].as_ref()) + .direction(Direction::Vertical) + .constraints([Constraint::Length(6), Constraint::Length(23)].as_ref()) .margin(1) - .split(help_body_area[0]); + .split(area); + // QR Code let qr_code = Paragraph::new(app_state.get_identity().qr_code.as_str()).block(Block::default()); + f.render_widget(qr_code, chunks[1]); - f.render_widget(qr_code, chunks[0]); - - let info_chunks = Layout::default() + // Connection details + let details_chunks = Layout::default() + .direction(Direction::Vertical) .constraints( [ - Constraint::Length(1), // Lining up fields with Qr Code - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Min(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), ] .as_ref(), ) - .horizontal_margin(1) - .split(chunks[1]); + .margin(1) + .split(chunks[0]); - // Public Key let block = Block::default() .borders(Borders::ALL) - .title(Span::styled("Public Key", Style::default().fg(Color::White))); - f.render_widget(block, info_chunks[1]); - let label_layout = Layout::default() - .constraints([Constraint::Length(1)].as_ref()) - .margin(1) - .split(info_chunks[1]); - // Put a space in front of pub key so it's easy to select - let public_key = Paragraph::new(format!(" {}", app_state.get_identity().public_key)); - f.render_widget(public_key, label_layout[0]); + .title(Span::styled("Connection Details", Style::default().fg(Color::White))); + f.render_widget(block, chunks[0]); + + const ITEM_01: &str = "Public Key: "; + const ITEM_02: &str = "Node ID: "; + const ITEM_03: &str = "Public Address: "; + const ITEM_04: &str = "Emoji ID: "; + + // Public Key + let public_key_text = Spans::from(vec![ + Span::styled(ITEM_01, Style::default().fg(Color::Magenta)), + Span::styled( + app_state.get_identity().public_key.clone(), + Style::default().fg(Color::White), + ), + ]); + let paragraph = Paragraph::new(public_key_text).block(Block::default()); + f.render_widget(paragraph, details_chunks[0]); // NodeId - let block = Block::default() - .borders(Borders::ALL) - .title(Span::styled("Node ID", Style::default().fg(Color::White))); - f.render_widget(block, info_chunks[2]); - let label_layout = Layout::default() - .constraints([Constraint::Length(1)].as_ref()) - .margin(1) - .split(info_chunks[2]); - let node_id = Paragraph::new(app_state.get_identity().node_id.as_str()); - f.render_widget(node_id, label_layout[0]); + let node_id_text = Spans::from(vec![ + Span::styled(ITEM_02, Style::default().fg(Color::Magenta)), + Span::styled( + app_state.get_identity().node_id.clone(), + Style::default().fg(Color::White), + ), + ]); + let paragraph = Paragraph::new(node_id_text).block(Block::default()); + f.render_widget(paragraph, details_chunks[1]); // Public Address - let block = Block::default() - .borders(Borders::ALL) - .title(Span::styled("Public Address", Style::default().fg(Color::White))); - f.render_widget(block, info_chunks[3]); - let label_layout = Layout::default() - .constraints([Constraint::Length(1)].as_ref()) - .margin(1) - .split(info_chunks[3]); - let public_address = Paragraph::new(format!(" {}", app_state.get_identity().public_address)); - f.render_widget(public_address, label_layout[0]); + let public_ddress_text = Spans::from(vec![ + Span::styled(ITEM_03, Style::default().fg(Color::Magenta)), + Span::styled( + app_state.get_identity().public_address.clone(), + Style::default().fg(Color::White), + ), + ]); + let paragraph = Paragraph::new(public_ddress_text).block(Block::default()); + f.render_widget(paragraph, details_chunks[2]); // Emoji ID - let block = Block::default() - .borders(Borders::ALL) - .title(Span::styled("Emoji ID", Style::default().fg(Color::White))); - f.render_widget(block, info_chunks[4]); - let label_layout = Layout::default() - .constraints([Constraint::Length(1)].as_ref()) - .margin(1) - .split(info_chunks[4]); - let emoji_id = Paragraph::new(app_state.get_identity().emoji_id.as_str()); - f.render_widget(emoji_id, label_layout[0]); + let emoji_id_text = Spans::from(vec![ + Span::styled(ITEM_04, Style::default().fg(Color::Magenta)), + Span::styled( + app_state.get_identity().emoji_id.clone(), + Style::default().fg(Color::White), + ), + ]); + let paragraph = Paragraph::new(emoji_id_text).block(Block::default()); + f.render_widget(paragraph, details_chunks[3]); } } diff --git a/applications/tari_console_wallet/src/ui/components/send_tab.rs b/applications/tari_console_wallet/src/ui/components/send_tab.rs index 2595deec50..614c0fb619 100644 --- a/applications/tari_console_wallet/src/ui/components/send_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/send_tab.rs @@ -7,7 +7,7 @@ use tui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Span, Spans}, - widgets::{Block, Borders, Clear, ListItem, Paragraph, Row, Table, TableState, Wrap}, + widgets::{Block, Borders, ListItem, Paragraph, Row, Table, TableState, Wrap}, Frame, }; use unicode_width::UnicodeWidthStr; @@ -16,7 +16,7 @@ use crate::{ ui::{ components::{balance::Balance, styles, Component, KeyHandled}, state::{AppState, UiTransactionSendStatus}, - widgets::{centered_rect_absolute, draw_dialog, MultiColumnList, WindowedListState}, + widgets::{draw_dialog, MultiColumnList, WindowedListState}, MAX_WIDTH, }, utils::formatting::display_compressed_string, @@ -25,15 +25,11 @@ use crate::{ pub struct SendTab { balance: Balance, send_input_mode: SendInputMode, - edit_contact_mode: ContactInputMode, show_contacts: bool, - show_edit_contact: bool, to_field: String, amount_field: String, fee_field: String, message_field: String, - alias_field: String, - public_key_field: String, error_message: Option, success_message: Option, contacts_list_state: WindowedListState, @@ -48,15 +44,11 @@ impl SendTab { Self { balance: Balance::new(), send_input_mode: SendInputMode::None, - edit_contact_mode: ContactInputMode::None, show_contacts: false, - show_edit_contact: false, to_field: String::new(), amount_field: String::new(), fee_field: app_state.get_default_fee_per_gram().as_u64().to_string(), message_field: String::new(), - alias_field: String::new(), - public_key_field: String::new(), error_message: None, success_message: None, contacts_list_state: WindowedListState::new(), @@ -211,14 +203,8 @@ impl SendTab { let instructions = Paragraph::new(Spans::from(vec![ Span::raw(" Use "), - Span::styled("Up/Down Arrow Keys", Style::default().add_modifier(Modifier::BOLD)), + Span::styled("Up↑/Down↓ Keys", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to choose a contact, "), - Span::styled("E", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to (e)dit and "), - Span::styled("D", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to (d)elete a contact, "), - Span::styled("N", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to create a (n)ew contact, "), Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), Span::raw(" to select."), ])) @@ -247,77 +233,14 @@ impl SendTab { .highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Magenta)) .heading_style(Style::default().fg(Color::Magenta)) .max_width(MAX_WIDTH) - .add_column(Some("Alias"), Some(12), column0_items) - .add_column(Some("Public Key"), Some(67), column1_items) + .add_column(Some("Alias"), Some(25), column0_items) + .add_column(None, Some(2), Vec::new()) + .add_column(Some("Public Key"), Some(64), column1_items) + .add_column(None, Some(2), Vec::new()) .add_column(Some("Emoji ID"), None, column2_items); column_list.render(f, list_areas[1], &mut list_state); } - fn draw_edit_contact(&mut self, f: &mut Frame, area: Rect, _app_state: &AppState) - where B: Backend { - let popup_area = centered_rect_absolute(120, 10, area); - - f.render_widget(Clear, popup_area); - - let block = Block::default().borders(Borders::ALL).title(Span::styled( - "Add/Edit Contact", - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), - )); - f.render_widget(block, popup_area); - let vert_chunks = Layout::default() - .constraints([Constraint::Length(2), Constraint::Length(3), Constraint::Length(3)].as_ref()) - .margin(1) - .split(popup_area); - - let instructions = Paragraph::new(Spans::from(vec![ - Span::raw("Press "), - Span::styled("L", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to edit "), - Span::styled("Alias", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" field, "), - Span::styled("K", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to edit "), - Span::styled("Public Key/Emoji ID", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" field, "), - Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to save Contact."), - ])) - .block(Block::default()); - f.render_widget(instructions, vert_chunks[0]); - - let alias_input = Paragraph::new(self.alias_field.as_ref()) - .style(match self.edit_contact_mode { - ContactInputMode::Alias => Style::default().fg(Color::Magenta), - _ => Style::default(), - }) - .block(Block::default().borders(Borders::ALL).title("A(l)ias:")); - f.render_widget(alias_input, vert_chunks[1]); - - let pubkey_input = Paragraph::new(self.public_key_field.as_ref()) - .style(match self.edit_contact_mode { - ContactInputMode::PubkeyEmojiId => Style::default().fg(Color::Magenta), - _ => Style::default(), - }) - .block(Block::default().borders(Borders::ALL).title("Public (K)ey / Emoji Id:")); - f.render_widget(pubkey_input, vert_chunks[2]); - - match self.edit_contact_mode { - ContactInputMode::None => (), - ContactInputMode::Alias => f.set_cursor( - // Put cursor past the end of the input text - vert_chunks[1].x + self.alias_field.width() as u16 + 1, - // Move one line down, from the border to the input line - vert_chunks[1].y + 1, - ), - ContactInputMode::PubkeyEmojiId => f.set_cursor( - // Put cursor past the end of the input text - vert_chunks[2].x + self.public_key_field.width() as u16 + 1, - // Move one line down, from the border to the input line - vert_chunks[2].y + 1, - ), - } - } - fn draw_tokens(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) where B: Backend { let tokens = app_state.get_owned_tokens(); @@ -446,23 +369,6 @@ impl SendTab { return KeyHandled::Handled; } }, - Some(ConfirmationDialogType::DeleteContact) => { - if 'y' == c { - if let Some(c) = self - .contacts_list_state - .selected() - .and_then(|i| app_state.get_contact(i)) - .cloned() - { - if let Err(_e) = Handle::current().block_on(app_state.delete_contact(c.public_key)) { - self.error_message = - Some("Could not delete selected contact\nPress Enter to continue.".to_string()); - } - } - self.confirmation_dialog = None; - return KeyHandled::Handled; - } - }, } } } @@ -520,74 +426,19 @@ impl SendTab { KeyHandled::NotHandled } - fn on_key_edit_contact(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { - if self.show_edit_contact && self.edit_contact_mode != ContactInputMode::None { - match self.edit_contact_mode { - ContactInputMode::None => return KeyHandled::Handled, - ContactInputMode::Alias => match c { - '\n' | '\t' => { - self.edit_contact_mode = ContactInputMode::PubkeyEmojiId; - return KeyHandled::Handled; - }, - c => { - self.alias_field.push(c); - return KeyHandled::Handled; - }, - }, - ContactInputMode::PubkeyEmojiId => match c { - '\n' => { - self.edit_contact_mode = ContactInputMode::None; - self.show_edit_contact = false; - - if let Err(_e) = Handle::current() - .block_on(app_state.upsert_contact(self.alias_field.clone(), self.public_key_field.clone())) - { - self.error_message = - Some("Invalid Public key or Emoji ID provided\n Press Enter to continue.".to_string()); - } - - self.alias_field = "".to_string(); - self.public_key_field = "".to_string(); - return KeyHandled::Handled; - }, - c => { - self.public_key_field.push(c); - return KeyHandled::Handled; - }, - }, - } - } - - KeyHandled::NotHandled - } - fn on_key_show_contacts(&mut self, c: char, app_state: &mut AppState) -> KeyHandled { - if self.show_contacts { - match c { - 'd' => { - self.confirmation_dialog = Some(ConfirmationDialogType::DeleteContact); - return KeyHandled::Handled; - }, - '\n' => { - if let Some(c) = self - .contacts_list_state - .selected() - .and_then(|i| app_state.get_contact(i)) - .cloned() - { - self.to_field = c.public_key; - self.send_input_mode = SendInputMode::Amount; - self.show_contacts = false; - } - return KeyHandled::Handled; - }, - 'n' => { - self.show_edit_contact = true; - self.edit_contact_mode = ContactInputMode::Alias; - return KeyHandled::Handled; - }, - _ => (), + if self.show_contacts && c == '\n' { + if let Some(c) = self + .contacts_list_state + .selected() + .and_then(|i| app_state.get_contact(i)) + .cloned() + { + self.to_field = c.public_key; + self.send_input_mode = SendInputMode::Amount; + self.show_contacts = false; } + return KeyHandled::Handled; } KeyHandled::NotHandled @@ -613,9 +464,6 @@ impl Component for SendTab { if self.show_contacts { self.draw_contacts(f, areas[2], app_state); - if self.show_edit_contact { - self.draw_edit_contact(f, area, app_state); - } }; if self.send_input_mode == SendInputMode::Amount { @@ -686,17 +534,6 @@ impl Component for SendTab { 9, ); }, - Some(ConfirmationDialogType::DeleteContact) => { - draw_dialog( - f, - area, - "Confirm Delete".to_string(), - "Are you sure you want to delete this contact?\n(Y)es / (N)o".to_string(), - Color::Red, - 120, - 9, - ); - }, } } @@ -727,10 +564,6 @@ impl Component for SendTab { return; } - if self.on_key_edit_contact(c, app_state) == KeyHandled::Handled { - return; - } - if self.on_key_show_contacts(c, app_state) == KeyHandled::Handled { return; } @@ -738,28 +571,6 @@ impl Component for SendTab { match c { 'c' => { self.show_contacts = !self.show_contacts; - if self.show_contacts { - self.show_edit_contact = false; - self.edit_contact_mode = ContactInputMode::Alias; - self.public_key_field = "".to_string(); - self.amount_field = "".to_string(); - self.message_field = "".to_string(); - self.send_input_mode = SendInputMode::None; - } - }, - 'e' => { - if let Some(c) = self - .contacts_list_state - .selected() - .and_then(|i| app_state.get_contact(i)) - { - self.public_key_field = c.public_key.clone(); - self.alias_field = c.alias.clone(); - if self.show_contacts { - self.show_edit_contact = true; - self.edit_contact_mode = ContactInputMode::Alias; - } - } }, 't' => self.send_input_mode = SendInputMode::To, 'a' => { @@ -835,10 +646,8 @@ impl Component for SendTab { } fn on_esc(&mut self, _: &mut AppState) { - self.edit_contact_mode = ContactInputMode::None; self.send_input_mode = SendInputMode::None; self.show_contacts = false; - self.show_edit_contact = false; } fn on_backspace(&mut self, _app_state: &mut AppState) { @@ -859,16 +668,6 @@ impl Component for SendTab { }, SendInputMode::None => {}, } - - match self.edit_contact_mode { - ContactInputMode::Alias => { - let _ = self.alias_field.pop(); - }, - ContactInputMode::PubkeyEmojiId => { - let _ = self.public_key_field.pop(); - }, - ContactInputMode::None => {}, - } } } @@ -881,16 +680,8 @@ pub enum SendInputMode { Fee, } -#[derive(PartialEq, Debug)] -pub enum ContactInputMode { - None, - Alias, - PubkeyEmojiId, -} - #[derive(PartialEq, Debug)] pub enum ConfirmationDialogType { NormalSend, OneSidedSend, - DeleteContact, } diff --git a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs index 3d1117c732..323df3f79f 100644 --- a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs @@ -477,18 +477,20 @@ impl Component for TransactionsTab { }; span_vec.push(Span::styled( - "Up/Down Arrow", + " Up↑/Down↓", Style::default().add_modifier(Modifier::BOLD), )); - span_vec.push(Span::raw(" selects a transaction, ")); + span_vec.push(Span::raw(" selects Tx, ")); span_vec.push(Span::styled("C", Style::default().add_modifier(Modifier::BOLD))); - span_vec.push(Span::raw(" cancels a selected Pending Tx, ")); + span_vec.push(Span::raw(" cancels selected Pending Tx, ")); span_vec.push(Span::styled("A", Style::default().add_modifier(Modifier::BOLD))); span_vec.push(Span::raw(" shows abandoned coinbase Txs, ")); + span_vec.push(Span::styled("R", Style::default().add_modifier(Modifier::BOLD))); + span_vec.push(Span::raw(" rebroadcast all Broadcast, ")); span_vec.push(Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD))); - span_vec.push(Span::raw(" exits the list. R: Rebroadcast all in Broadcast")); + span_vec.push(Span::raw(" exits list.")); - let instructions = Paragraph::new(Spans::from(span_vec)).wrap(Wrap { trim: true }); + let instructions = Paragraph::new(Spans::from(span_vec)).wrap(Wrap { trim: false }); f.render_widget(instructions, areas[1]); self.draw_transaction_lists(f, areas[2], app_state); diff --git a/base_layer/common_types/src/transaction.rs b/base_layer/common_types/src/transaction.rs index 45908a7c89..94ef6d12b7 100644 --- a/base_layer/common_types/src/transaction.rs +++ b/base_layer/common_types/src/transaction.rs @@ -35,18 +35,10 @@ pub enum TransactionStatus { impl TransactionStatus { pub fn is_faux(&self) -> bool { - match self { - TransactionStatus::Completed => false, - TransactionStatus::Broadcast => false, - TransactionStatus::MinedUnconfirmed => false, - TransactionStatus::Imported => true, - TransactionStatus::Pending => false, - TransactionStatus::Coinbase => false, - TransactionStatus::MinedConfirmed => false, - TransactionStatus::Rejected => false, - TransactionStatus::FauxUnconfirmed => true, - TransactionStatus::FauxConfirmed => true, - } + matches!( + self, + TransactionStatus::Imported | TransactionStatus::FauxUnconfirmed | TransactionStatus::FauxConfirmed + ) } } From ee4b52d97cb41133dbf1ed9dd2f0787fc00375d2 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Tue, 15 Feb 2022 12:29:07 +0400 Subject: [PATCH 17/28] feat(validator-node): initial state sync implementation (partial) (#3826) Description --- - add OpLogs - add comms RPC methods and client calls to sync validator node state - add synchronising state and basic implementation to validator node state machine (currently always in sync due to incorrect Merkle root in initial checkpoint) - fix: fetching checkpoint from base node returned incorrect field for asset public key - add read only interface for state db Motivation and Context --- Validator node on a committee needs to sync their state after going offline. How Has This Been Tested? --- Manually, it didnt work because the merkle root of the initial checkpoint is incorrect. Sync component has been hardcoded to always say it is in sync for now. --- Cargo.lock | 889 ++++++++++-------- .../backend/src/commands/create_workspace.rs | 8 +- applications/launchpad/backend/src/main.rs | 14 +- .../src/grpc/base_node_grpc_server.rs | 4 +- applications/tari_base_node/src/main.rs | 2 +- .../tari_collectibles/src-tauri/Cargo.toml | 3 +- .../src-tauri/{src => }/build.rs | 0 .../proto/dan/validator_node.proto | 43 +- .../tari_validator_node/src/dan_node.rs | 24 +- .../src/grpc/services/base_node_client.rs | 40 +- .../src/grpc/validator_node_grpc_server.rs | 4 +- applications/tari_validator_node/src/main.rs | 8 +- .../src/p2p/proto/conversions.rs | 115 ++- .../tari_validator_node/src/p2p/rpc/mod.rs | 18 + .../src/p2p/rpc/service_impl.rs | 107 ++- .../services/inbound_connection_service.rs | 10 +- .../src/p2p/services/rpc_client.rs | 120 ++- .../configuration/validator_node_config.rs | 8 +- .../common_types/proto/tips/tip002.proto | 4 +- dan_layer/core/Cargo.toml | 1 + dan_layer/core/src/digital_assets_error.rs | 21 +- dan_layer/core/src/fixed_hash.rs | 16 +- dan_layer/core/src/models/asset_definition.rs | 11 + .../core/src/models/base_layer_output.rs | 57 +- .../consensus_worker_domain_event.rs | 6 +- dan_layer/core/src/models/error.rs | 8 + .../core/src/models/hot_stuff_tree_node.rs | 2 +- dan_layer/core/src/models/mod.rs | 7 +- dan_layer/core/src/models/op_log.rs | 69 ++ dan_layer/core/src/models/state_root.rs | 14 +- dan_layer/core/src/models/tari_dan_payload.rs | 17 +- .../core/src/services/asset_processor.rs | 10 +- dan_layer/core/src/services/asset_proxy.rs | 10 +- .../core/src/services/base_node_client.rs | 5 + .../core/src/services/events_publisher.rs | 2 +- dan_layer/core/src/services/mocks/mod.rs | 11 +- dan_layer/core/src/services/mod.rs | 2 +- .../src/services/validator_node_rpc_client.rs | 44 +- dan_layer/core/src/storage/chain/chain_db.rs | 7 +- .../storage/chain/chain_db_backend_adapter.rs | 3 +- .../storage/chain/chain_db_unit_of_work.rs | 16 +- dan_layer/core/src/storage/mocks/chain_db.rs | 15 + dan_layer/core/src/storage/mocks/state_db.rs | 22 +- .../core/src/storage/state/db_key_value.rs | 2 +- dan_layer/core/src/storage/state/mod.rs | 8 +- dan_layer/core/src/storage/state/state_db.rs | 22 +- .../storage/state/state_db_backend_adapter.rs | 13 +- .../storage/state/state_db_unit_of_work.rs | 218 ++++- .../core/src/storage/state/state_op_log.rs | 77 ++ .../core/src/templates/tip002_template.rs | 10 +- .../core/src/templates/tip004_template.rs | 11 +- .../core/src/templates/tip721_template.rs | 9 +- .../core/src/workers/consensus_worker.rs | 56 +- dan_layer/core/src/workers/mod.rs | 3 + .../core/src/workers/state_sync/error.rs | 39 + dan_layer/core/src/workers/state_sync/mod.rs | 141 +++ .../core/src/workers/states/commit_state.rs | 49 +- dan_layer/core/src/workers/states/mod.rs | 4 + .../src/workers/states/pre_commit_state.rs | 52 +- dan_layer/core/src/workers/states/prepare.rs | 7 +- dan_layer/core/src/workers/states/starting.rs | 3 +- .../core/src/workers/states/synchronizing.rs | 119 +++ .../down.sql | 1 + .../up.sql | 13 + dan_layer/storage_sqlite/src/error.rs | 2 + dan_layer/storage_sqlite/src/models/mod.rs | 1 + dan_layer/storage_sqlite/src/models/node.rs | 4 +- .../storage_sqlite/src/models/state_op_log.rs | 86 ++ dan_layer/storage_sqlite/src/schema.rs | 22 +- .../src/sqlite_chain_backend_adapter.rs | 70 +- .../src/sqlite_state_db_backend_adapter.rs | 58 +- 71 files changed, 2215 insertions(+), 682 deletions(-) rename applications/tari_collectibles/src-tauri/{src => }/build.rs (100%) create mode 100644 dan_layer/core/src/models/op_log.rs create mode 100644 dan_layer/core/src/storage/state/state_op_log.rs create mode 100644 dan_layer/core/src/workers/state_sync/error.rs create mode 100644 dan_layer/core/src/workers/state_sync/mod.rs create mode 100644 dan_layer/core/src/workers/states/synchronizing.rs create mode 100644 dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/down.sql create mode 100644 dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/up.sql create mode 100644 dan_layer/storage_sqlite/src/models/state_op_log.rs diff --git a/Cargo.lock b/Cargo.lock index b129cb229a..9453467596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,20 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "ashpd" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7915e26e0786f91768d23de32afafa4ee5e2ea76be21c0ecd8e14441543c1655" +dependencies = [ + "enumflags2", + "futures 0.3.21", + "rand 0.8.4", + "serde 1.0.136", + "serde_repr", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.3.4" @@ -297,9 +311,9 @@ dependencies = [ [[package]] name = "atk" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a83b21d2aa75e464db56225e1bda2dd5993311ba1095acaa8fa03d1ae67026ba" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", "bitflags 1.3.2", @@ -309,21 +323,21 @@ dependencies = [ [[package]] name = "atk-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badcf670157c84bb8b1cf6b5f70b650fed78da2033c9eed84c4e49b11cbe83ea" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" dependencies = [ - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] name = "attohttpc" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d" +checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" dependencies = [ "flate2", "http", @@ -332,7 +346,7 @@ dependencies = [ "openssl", "serde 1.0.136", "serde_json", - "serde_urlencoded 0.6.1", + "serde_urlencoded", "url 2.2.2", "wildmatch", ] @@ -517,7 +531,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq", - "digest 0.10.1", + "digest 0.10.2", "rayon", ] @@ -625,7 +639,7 @@ dependencies = [ "serde 1.0.136", "serde_derive", "serde_json", - "serde_urlencoded 0.7.1", + "serde_urlencoded", "thiserror", "tokio 1.16.1", "tokio-util", @@ -764,9 +778,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cairo-rs" -version = "0.14.9" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b5725979db0c586d98abad2193cdb612dd40ef95cd26bd99851bf93b3cb482" +checksum = "b869e97a87170f96762f9f178eae8c461147e722ba21dd8814105bf5716bf14a" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -777,13 +791,24 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.14.9" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b448b876970834fda82ba3aeaccadbd760206b75388fc5c1b02f1e343b697570" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ - "glib-sys 0.14.0", + "glib-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", +] + +[[package]] +name = "cargo_toml" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e270ef0cd868745878982f7ce470aa898d0d4bb248af67f0cf66f54617913ef" +dependencies = [ + "serde 1.0.136", + "serde_derive", + "toml 0.5.8", ] [[package]] @@ -877,6 +902,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -1002,34 +1036,17 @@ dependencies = [ [[package]] name = "clap" -version = "3.0.0-beta.2" +version = "3.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" dependencies = [ "atty", "bitflags 1.3.2", - "clap_derive", "indexmap", - "lazy_static 1.4.0", "os_str_bytes", "strsim 0.10.0", "termcolor", - "textwrap 0.12.1", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clap_derive" -version = "3.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" -dependencies = [ - "heck 0.4.0", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "textwrap 0.14.2", ] [[package]] @@ -1070,8 +1087,8 @@ dependencies = [ "bitflags 1.3.2", "block", "cocoa-foundation", - "core-foundation 0.9.3", - "core-graphics 0.22.3", + "core-foundation", + "core-graphics", "foreign-types", "libc", "objc", @@ -1085,7 +1102,7 @@ checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" dependencies = [ "bitflags 1.3.2", "block", - "core-foundation 0.9.3", + "core-foundation", "core-graphics-types", "foreign-types", "libc", @@ -1098,37 +1115,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" -[[package]] -name = "com" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a30a2b2a013da986dc5cc3eda3d19c0d59d53f835be1b2356eb8d00f000c793" -dependencies = [ - "com_macros", -] - -[[package]] -name = "com_macros" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7606b05842fea68ddcc89e8053b8860ebcb2a0ba8d6abfe3a148e5d5a8d3f0c1" -dependencies = [ - "com_macros_support", - "proc-macro2", - "syn", -] - -[[package]] -name = "com_macros_support" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97e9a6d20f4ac8830e309a455d7e9416e65c6af5a97c88c55fbb4c2012e107da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "concurrent-queue" version = "1.2.2" @@ -1192,50 +1178,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys", "libc", ] -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.7.0", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.22.3" @@ -1243,7 +1201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation", "core-graphics-types", "foreign-types", "libc", @@ -1256,24 +1214,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", + "core-foundation", "foreign-types", "libc", ] -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "objc", -] - [[package]] name = "cpufeatures" version = "0.2.1" @@ -1534,9 +1479,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06" dependencies = [ "generic-array 0.14.5", ] @@ -1600,6 +1545,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctr" version = "0.6.0" @@ -1902,13 +1857,12 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837" dependencies = [ "block-buffer 0.10.2", "crypto-common", - "generic-array 0.14.5", "subtle", ] @@ -2000,7 +1954,7 @@ dependencies = [ "ed25519", "rand 0.7.3", "serde 1.0.136", - "sha2", + "sha2 0.9.9", "zeroize", ] @@ -2471,9 +2425,9 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "gdk" -version = "0.14.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d749dcfc00d8de0d7c3a289e04a04293eb5ba3d8a4e64d64911d481fa9933b" +checksum = "614258e81ec35ed8770e64a0838f3a47f95b398bc51e724d3b3fa09c1ee0f8d5" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -2487,10 +2441,11 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.14.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534192cb8f01daeb8fab2c8d4baa8f9aae5b7a39130525779f5c2608e235b10f" +checksum = "73aa2f5de1b45710da90a55863276667dc3a3264aaf6a2aeace62bb015244d49" dependencies = [ + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -2499,32 +2454,45 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f097c0704201fbc8f69c1762dc58c6947c8bb188b8ed0bc7e65259f1894fe590" +checksum = "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171" dependencies = [ - "gio-sys 0.14.0", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "gio-sys 0.15.5", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] name = "gdk-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e091b3d3d6696949ac3b3fb3c62090e5bfd7bd6850bef5c3c5ea701de1b1f1e" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.14.0", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "gio-sys 0.15.5", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", "pango-sys", "pkg-config", - "system-deps 3.2.0", + "system-deps 6.0.1", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys 0.15.5", + "libc", + "system-deps 6.0.1", + "x11", ] [[package]] @@ -2627,15 +2595,15 @@ dependencies = [ [[package]] name = "gio" -version = "0.14.8" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711c3632b3ebd095578a9c091418d10fed492da9443f58ebc8f45efbeb215cb0" +checksum = "59105fa464928adf56b159c8d980cc11fbfbe414befb904caac5163d383049bf" dependencies = [ "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", - "gio-sys 0.14.0", + "gio-sys 0.15.5", "glib", "libc", "once_cell", @@ -2644,27 +2612,27 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e24fb752f8f5d2cf6bbc2c606fd2bc989c81c5e2fe321ab974d54f8b6344eac" +checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" dependencies = [ - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "glib-sys 0.14.0", + "gobject-sys 0.14.0", "libc", - "system-deps 1.3.2", + "system-deps 3.2.0", "winapi 0.3.9", ] [[package]] name = "gio-sys" -version = "0.14.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" +checksum = "4f0bc4cfc9ebcdd05cc5057bc51b99c32f8f9bf246274f6a556ffd27279f8fe3" dependencies = [ - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", "winapi 0.3.9", ] @@ -2685,9 +2653,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.14.8" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" +checksum = "41dcfbdb6cc6c02aee163339465d8a40d6f3f64c3a43f729a4195f0e153338b7" dependencies = [ "bitflags 1.3.2", "futures-channel", @@ -2695,21 +2663,22 @@ dependencies = [ "futures-executor", "futures-task", "glib-macros", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", "once_cell", "smallvec", + "thiserror", ] [[package]] name = "glib-macros" -version = "0.14.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" +checksum = "e58b262ff65ef771003873cea8c10e0fe854f1c508d48d62a4111a1ff163f7d1" dependencies = [ "anyhow", - "heck 0.3.3", + "heck 0.4.0", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -2719,22 +2688,22 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" dependencies = [ "libc", - "system-deps 1.3.2", + "system-deps 3.2.0", ] [[package]] name = "glib-sys" -version = "0.14.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +checksum = "fa1d4e1a63d8574541e5b92931e4e669ddc87ffa85d58e84e631dba13ad2e10c" dependencies = [ "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] @@ -2758,31 +2727,31 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.10.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" dependencies = [ - "glib-sys 0.10.1", + "glib-sys 0.14.0", "libc", - "system-deps 1.3.2", + "system-deps 3.2.0", ] [[package]] name = "gobject-sys" -version = "0.14.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +checksum = "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a" dependencies = [ - "glib-sys 0.14.0", + "glib-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] name = "gtk" -version = "0.14.3" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb51122dd3317e9327ec1e4faa151d1fa0d95664cd8fb8dcfacf4d4d29ac70c" +checksum = "c7978eaec05bea63947c801d29a21372f2ed39aec0bf56bf7725d3599094675e" dependencies = [ "atk", "bitflags 1.3.2", @@ -2803,30 +2772,29 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.14.0" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c14c8d3da0545785a7c5a120345b3abb534010fb8ae0f2ef3f47c027fba303e" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.14.0", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "gio-sys 0.15.5", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", "pango-sys", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] name = "gtk3-macros" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" +checksum = "8c891188af69e77a1e8a0b1746fbd03b9b396e7d34d518c5331b15950259f541" dependencies = [ "anyhow", - "heck 0.3.3", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -2875,9 +2843,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84c647447a07ca16f5fbd05b633e535cc41a08d2d74ab1e08648df53be9cb89" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ "base64 0.13.0", "bitflags 1.3.2", @@ -2886,7 +2854,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.9.8", + "sha-1 0.10.0", ] [[package]] @@ -3005,9 +2973,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.16" +version = "0.14.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ec3e62bdc98a2f0393a5048e4c30ef659440ea6e0e572965103e72bd836f55" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" dependencies = [ "bytes 1.1.0", "futures-channel", @@ -3018,7 +2986,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 0.4.8", + "itoa 1.0.1", "pin-project-lite 0.2.8", "socket2", "tokio 1.16.1", @@ -3240,21 +3208,25 @@ checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "javascriptcore-rs" -version = "0.14.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9c7d1445bba2889672fbadc16c3d5007bfdcf0a15a18a3a50fe9fab2c7427" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" dependencies = [ + "bitflags 1.3.2", "glib", "javascriptcore-rs-sys", ] [[package]] name = "javascriptcore-rs-sys" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f46ada8a08dcd75a10afae872fbfb51275df4a8ae0d46b8cc7c708f08dd2998" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" dependencies = [ + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", + "system-deps 5.0.0", ] [[package]] @@ -3281,6 +3253,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f995a3c8f2bc3dd52a18a583e90f9ec109c047fa1603a853e46bcda14d2e279d" +dependencies = [ + "serde 1.0.136", + "serde_json", + "treediff", +] + [[package]] name = "json5" version = "0.2.8" @@ -3772,12 +3755,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minisign-verify" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0507fe8e3c68cd62961cf9f87f6c2b21d884d3515a7150a4a3fa9d014e5c12" - [[package]] name = "miniz_oxide" version = "0.3.7" @@ -4074,9 +4051,9 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ "winapi 0.3.9", ] @@ -4229,6 +4206,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -4242,6 +4220,15 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -4424,9 +4411,9 @@ dependencies = [ [[package]] name = "os_pipe" -version = "0.9.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +checksum = "0e3492ebca331b895fe23ed427dce2013d9b2e00c45964f12040b0db38b8ab27" dependencies = [ "libc", "winapi 0.3.9", @@ -4434,9 +4421,12 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "2.4.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] [[package]] name = "packed_simd_2" @@ -4450,9 +4440,9 @@ dependencies = [ [[package]] name = "pango" -version = "0.14.8" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "546fd59801e5ca735af82839007edd226fe7d3bb06433ec48072be4439c28581" +checksum = "79211eff430c29cc38c69e0ab54bc78fa1568121ca9737707eee7f92a8417a94" dependencies = [ "bitflags 1.3.2", "glib", @@ -4463,14 +4453,14 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2367099ca5e761546ba1d501955079f097caa186bb53ce0f718dca99ac1942fe" +checksum = "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2" dependencies = [ - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "libc", - "system-deps 3.2.0", + "system-deps 6.0.1", ] [[package]] @@ -4693,7 +4683,7 @@ dependencies = [ "ripemd160", "rsa", "sha-1 0.9.8", - "sha2", + "sha2 0.9.9", "sha3", "signature", "smallvec", @@ -4903,6 +4893,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + [[package]] name = "poly1305" version = "0.7.2" @@ -5268,16 +5264,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "raw-window-handle" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28f55143d0548dad60bb4fbdc835a3d7ac6acc3324506450c5fdd6e42903a76" -dependencies = [ - "libc", - "raw-window-handle 0.4.2", -] - [[package]] name = "raw-window-handle" version = "0.4.2" @@ -5418,7 +5404,7 @@ dependencies = [ "pin-project-lite 0.2.8", "serde 1.0.136", "serde_json", - "serde_urlencoded 0.7.1", + "serde_urlencoded", "tokio 1.16.1", "tokio-native-tls", "url 2.2.2", @@ -5430,25 +5416,28 @@ dependencies = [ [[package]] name = "rfd" -version = "0.4.4" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609ed912e110af7d7084b6b17d2a68b25e766208e015a37beba1be3c2d7cbb3b" +checksum = "2aaf1d71ccd44689f7c2c72da1117fd8db71f72a76fe9b5c5dbb17ab903007e0" dependencies = [ + "ashpd", "block", "dispatch", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "gtk-sys", "js-sys", "lazy_static 1.4.0", + "log", "objc", "objc-foundation", "objc_id", - "raw-window-handle 0.3.4", + "pollster", + "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winapi 0.3.9", + "windows 0.30.0", ] [[package]] @@ -5523,7 +5512,7 @@ dependencies = [ "num-traits 0.2.14", "pem", "rand 0.7.3", - "sha2", + "sha2 0.9.9", "simple_asn1", "subtle", "thiserror", @@ -5722,8 +5711,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.3", - "core-foundation-sys 0.8.3", + "core-foundation", + "core-foundation-sys", "libc", "security-framework-sys", ] @@ -5734,7 +5723,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys", "libc", ] @@ -5894,18 +5883,6 @@ dependencies = [ "serde 0.8.23", ] -[[package]] -name = "serde_urlencoded" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec5d77e2d4c73717816afac02670d5c4f534ea95ed430442cad02e7a6e32c97" -dependencies = [ - "dtoa", - "itoa 0.4.8", - "serde 1.0.136", - "url 2.2.2", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5953,6 +5930,28 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde 1.0.136", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -5988,6 +5987,17 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.2", +] + [[package]] name = "sha1" version = "0.6.0" @@ -6007,6 +6017,17 @@ dependencies = [ "opaque-debug 0.3.0", ] +[[package]] +name = "sha2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.2", +] + [[package]] name = "sha3" version = "0.9.1" @@ -6030,9 +6051,9 @@ dependencies = [ [[package]] name = "shared_child" -version = "0.3.5" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" dependencies = [ "libc", "winapi 0.3.9", @@ -6132,7 +6153,7 @@ dependencies = [ "rand 0.8.4", "rand_core 0.6.3", "rustc_version 0.3.3", - "sha2", + "sha2 0.9.9", "subtle", "x25519-dalek", ] @@ -6148,18 +6169,17 @@ dependencies = [ ] [[package]] -name = "soup-sys" -version = "0.10.0" +name = "soup2-sys" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7adf08565630bbb71f955f11f8a68464817ded2703a3549747c235b58a13e" +checksum = "9f056675eda9a7417163e5f742bb119e8e1d385edd2ada8f7031a7230a3ec10a" dependencies = [ "bitflags 1.3.2", - "gio-sys 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", + "gio-sys 0.14.0", + "glib-sys 0.14.0", + "gobject-sys 0.14.0", "libc", - "pkg-config", - "system-deps 1.3.2", + "system-deps 5.0.0", ] [[package]] @@ -6269,12 +6289,6 @@ dependencies = [ "syn", ] -[[package]] -name = "strum" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" - [[package]] name = "strum" version = "0.21.0" @@ -6296,18 +6310,6 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" -[[package]] -name = "strum_macros" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "strum_macros" version = "0.21.1" @@ -6387,18 +6389,13 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "1.3.2" +name = "sys-info" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" dependencies = [ - "heck 0.3.3", - "pkg-config", - "strum 0.18.0", - "strum_macros 0.18.0", - "thiserror", - "toml 0.5.8", - "version-compare 0.0.10", + "cc", + "libc", ] [[package]] @@ -6408,7 +6405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" dependencies = [ "anyhow", - "cfg-expr", + "cfg-expr 0.8.1", "heck 0.3.3", "itertools 0.10.3", "pkg-config", @@ -6419,27 +6416,53 @@ dependencies = [ "version-compare 0.0.11", ] +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.8", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3a97fdef3daf935d929b3e97e5a6a680cd4622e40c2941ca0875d6566416f8" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.4.0", + "pkg-config", + "toml 0.5.8", + "version-compare 0.1.0", +] + [[package]] name = "tao" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa57de7c282b68f8906278543a724ed8f5a2568f069dd0cc05fc10d1f07036b" +checksum = "402f170c4da28cc3108d854c7567d6904c82fc0e30e5b69f2a6b3e48f8a4f705" dependencies = [ "bitflags 1.3.2", "cairo-rs", "cc", "cocoa", - "core-foundation 0.9.3", - "core-graphics 0.22.3", - "core-video-sys", + "core-foundation", + "core-graphics", "crossbeam-channel 0.5.2", "dispatch", "gdk", "gdk-pixbuf", "gdk-sys", + "gdkx11-sys", "gio", "glib", - "glib-sys 0.14.0", + "glib-sys 0.15.5", "gtk", "instant", "lazy_static 1.4.0", @@ -6450,14 +6473,28 @@ dependencies = [ "ndk-sys", "objc", "parking_lot 0.11.2", - "raw-window-handle 0.3.4", + "raw-window-handle", "scopeguard", "serde 1.0.136", + "tao-core-video-sys", "unicode-segmentation", - "winapi 0.3.9", + "windows 0.30.0", + "windows_macros", "x11-dl", ] +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if 1.0.0", + "core-foundation-sys", + "libc", + "objc", +] + [[package]] name = "tar" version = "0.4.38" @@ -6620,7 +6657,7 @@ dependencies = [ "prost-build", "serde 1.0.136", "serde_json", - "sha2", + "sha2 0.9.9", "structopt", "tari_storage", "tari_test_utils 0.28.0", @@ -6778,7 +6815,7 @@ dependencies = [ "regex", "rpassword", "rustyline", - "sha2", + "sha2 0.9.9", "strum 0.22.0", "strum_macros 0.22.0", "tari_app_grpc", @@ -6880,7 +6917,7 @@ dependencies = [ "rmp-serde", "serde 1.0.136", "serde_json", - "sha2", + "sha2 0.9.9", "sha3", "tari_bulletproofs", "tari_utilities", @@ -6912,6 +6949,7 @@ dependencies = [ "patricia_tree", "prost", "prost-types", + "rand 0.8.4", "serde 1.0.136", "serde_json", "tari_common", @@ -6972,7 +7010,7 @@ dependencies = [ "serde 1.0.136", "serde_derive", "serde_json", - "sha2", + "sha2 0.9.9", "strum 0.22.0", "strum_macros 0.22.0", "tari_common_types", @@ -7358,7 +7396,7 @@ dependencies = [ "rand 0.8.4", "serde 1.0.136", "serde_json", - "sha2", + "sha2 0.9.9", "strum 0.22.0", "strum_macros 0.22.0", "tari_common", @@ -7413,15 +7451,14 @@ dependencies = [ [[package]] name = "tauri" -version = "1.0.0-beta.8" +version = "1.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a0579dcc6fb883fe90dd3c66d76b8b8f4a1786e1e915e314b2017a500ede09" +checksum = "05cd4bbdc3d855aa78bad4c7c39bf425d51f09d614d75fff2493c70aa89e16f4" dependencies = [ "attohttpc", - "base64 0.13.0", "bincode", "cfg_aliases", - "clap 3.0.0-beta.2", + "clap 3.0.14", "dirs-next 2.0.0", "either", "embed_plist", @@ -7429,10 +7466,10 @@ dependencies = [ "futures 0.3.21", "futures-lite", "glib", + "glob", "gtk", "http", "ignore", - "minisign-verify", "notify-rust", "once_cell", "open", @@ -7440,12 +7477,14 @@ dependencies = [ "os_pipe", "percent-encoding 2.1.0", "rand 0.8.4", - "raw-window-handle 0.3.4", + "raw-window-handle", + "regex", "rfd", "semver 1.0.5", "serde 1.0.136", "serde_json", "serde_repr", + "serialize-to-javascript", "shared_child", "state", "tar", @@ -7463,13 +7502,12 @@ dependencies = [ [[package]] name = "tauri-build" -version = "1.0.0-beta.4" +version = "1.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9c9a9bea25b9d6f5845b8662e18447e17218f99860cab37e39e2b57a9fcd49" +checksum = "53075d8ce33827474e1c97ef5c75e4a007e9cae81dba4ce8c61f29a459b5aae8" dependencies = [ "anyhow", - "proc-macro2", - "quote", + "cargo_toml", "serde_json", "tauri-utils", "winres", @@ -7477,40 +7515,44 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.0.0-beta.4" +version = "1.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1663739ab53e281919676f216fb56a031104d0d2cd1a2dd5b012d279bcdb0ea4" +checksum = "a52763b36186053eeede537589a7373d8a253de5ac4c6235ad104c2c971343ce" dependencies = [ + "base64 0.13.0", "blake3", - "kuchiki", "proc-macro2", "quote", "regex", "serde 1.0.136", "serde_json", + "sha2 0.10.1", "tauri-utils", "thiserror", + "uuid", "walkdir", "zstd", ] [[package]] name = "tauri-macros" -version = "1.0.0-beta.5" +version = "1.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddf9f5868402323f35ef94fa6ab1d5d10b29aea9de598d829723aa1db5693b4" +checksum = "5493f2bcfc0634e77ace91d83820d4f7f597b94ff2314cd4b3431caf020a5558" dependencies = [ + "heck 0.4.0", "proc-macro2", "quote", "syn", "tauri-codegen", + "tauri-utils", ] [[package]] name = "tauri-runtime" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c52eccfb7f2ce5a09262bdc3671f0f07f637e27f8aa25e5f38145cddcd4e01" +checksum = "be5d4967753541a0d1ac324c50c2c381811fbdc3e743ad0a8b6132f55143ace5" dependencies = [ "gtk", "http", @@ -7521,14 +7563,15 @@ dependencies = [ "tauri-utils", "thiserror", "uuid", - "winapi 0.3.9", + "webview2-com", + "windows 0.30.0", ] [[package]] name = "tauri-runtime-wry" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fed8dd0a448c303fa764859d6dfa1c746c4f2c6c30a83c162f8bebb12e4af4e" +checksum = "61854d1fb4ebf92044a9885cf64e04e515fa1d50daa9390a8ab373b7b2a2b21c" dependencies = [ "gtk", "ico", @@ -7537,25 +7580,33 @@ dependencies = [ "tauri-runtime", "tauri-utils", "uuid", - "winapi 0.3.9", + "webview2-com", + "windows 0.30.0", "wry", ] [[package]] name = "tauri-utils" -version = "1.0.0-beta.3" +version = "1.0.0-rc.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb9b79594f22b6ed0cc8362e0dfde5b7969962de3cd8ca683de702e59e8221b" +checksum = "355ec32fbbc19eadc28adc24a90882e5dac111d779fda60cb05c02b56080df5f" dependencies = [ + "ctor", + "glob", + "heck 0.4.0", "html5ever", + "json-patch", "kuchiki", "phf 0.10.1", "proc-macro2", "quote", "serde 1.0.136", "serde_json", + "serde_with", + "serialize-to-javascript", "thiserror", "url 2.2.2", + "walkdir", "zstd", ] @@ -7618,12 +7669,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.12.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" -dependencies = [ - "unicode-width", -] +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" [[package]] name = "thin-slice" @@ -8109,6 +8157,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079" +[[package]] +name = "treediff" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e8d5ad7ce14bb82b7e61ccc0ca961005a275a060b9644a2431aa11553c2ff" +dependencies = [ + "serde_json", +] + [[package]] name = "trust-dns-client" version = "0.21.0-alpha.5" @@ -8373,15 +8430,15 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version-compare" -version = "0.0.10" +version = "0.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" [[package]] name = "version-compare" -version = "0.0.11" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" [[package]] name = "version_check" @@ -8442,7 +8499,7 @@ dependencies = [ "scoped-tls", "serde 1.0.136", "serde_json", - "serde_urlencoded 0.7.1", + "serde_urlencoded", "tokio 1.16.1", "tokio-stream", "tokio-util", @@ -8566,19 +8623,19 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "0.14.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e47b7f870883fc21612d2a51b74262f7f2cc5371f1621370817292a35300a9" +checksum = "2cbd39499e917de9dad36eb11c09f665eb984d432638ae7971feed98eb96df88" dependencies = [ "bitflags 1.3.2", "cairo-rs", "gdk", "gdk-sys", "gio", - "gio-sys 0.14.0", + "gio-sys 0.15.5", "glib", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "gtk", "gtk-sys", "javascriptcore-rs", @@ -8589,25 +8646,25 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66ccc9f0cb4de7c3b92376a5bf64e7ddffb33852f092721731a039ec38dda98" +checksum = "ddcce6f1e0fc7715d651dba29875741509f5fc12f4e2976907272a74405f2b01" dependencies = [ "atk-sys", "bitflags 1.3.2", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.14.0", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "gio-sys 0.15.5", + "glib-sys 0.15.5", + "gobject-sys 0.15.5", "gtk-sys", "javascriptcore-rs-sys", "libc", "pango-sys", "pkg-config", - "soup-sys", - "system-deps 3.2.0", + "soup2-sys", + "system-deps 5.0.0", ] [[package]] @@ -8631,26 +8688,40 @@ dependencies = [ ] [[package]] -name = "webview2" -version = "0.1.4" +name = "webview2-com" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283bf6b0ed9c83faea8c7bfe40bb261592147a109effaa4077eed294863d5031" +checksum = "1975ce3573344c099935fe3903f1708dac69efe8539f1efee3ae54d8f9315fbb" dependencies = [ - "com", - "once_cell", - "webview2-sys", - "widestring", - "winapi 0.3.9", + "webview2-com-macros", + "webview2-com-sys", + "windows 0.30.0", + "windows_macros", ] [[package]] -name = "webview2-sys" -version = "0.1.1" +name = "webview2-com-macros" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24b7889e893ac4c50d7346356be3ce13a85e56512c38b8fde0526559b8012a4c" +checksum = "1515c6c82fcee93f6edaacc72c8e233dbe4ff3ca569dce1901dfc36c404a3e99" dependencies = [ - "com", - "winapi 0.3.9", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "webview2-com-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a746838a94b7391f707209a246e3436d81d1e71832126a65a897d3ee5511040" +dependencies = [ + "regex", + "serde 1.0.136", + "serde_json", + "thiserror", + "windows 0.30.0", + "windows-bindgen", ] [[package]] @@ -8682,17 +8753,11 @@ dependencies = [ "libc", ] -[[package]] -name = "widestring" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" - [[package]] name = "wildmatch" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a" +checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" [[package]] name = "winapi" @@ -8749,6 +8814,29 @@ dependencies = [ "windows_x86_64_msvc 0.24.0", ] +[[package]] +name = "windows" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu 0.30.0", + "windows_i686_msvc 0.30.0", + "windows_x86_64_gnu 0.30.0", + "windows_x86_64_msvc 0.30.0", +] + +[[package]] +name = "windows-bindgen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944c545fcae9dd66488308f8b69aa3ba34f53714416ecfcdcbbfa4b6821e27c6" +dependencies = [ + "windows_quote", + "windows_reader", +] + [[package]] name = "windows-sys" version = "0.30.0" @@ -8768,6 +8856,16 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" +[[package]] +name = "windows_gen" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30dff4d91d22520628bb94b66f2bb313cb16a09a515a32320a84a1b449bc94c0" +dependencies = [ + "windows_quote", + "windows_reader", +] + [[package]] name = "windows_i686_gnu" version = "0.24.0" @@ -8792,6 +8890,30 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" +[[package]] +name = "windows_macros" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ae44ab917e9005fe710d99d52d227ca0164b10a09be90649142cc3fab825d3" +dependencies = [ + "syn", + "windows_gen", + "windows_quote", + "windows_reader", +] + +[[package]] +name = "windows_quote" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f02c51a77e6248c1206aaa920802c32d50a05205e229b118d7f3afd3036667" + +[[package]] +name = "windows_reader" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44e6df0da993cda589c5ac852272fbb2a0ead67a031a017dd3eac11528a2d72" + [[package]] name = "windows_x86_64_gnu" version = "0.24.0" @@ -8841,18 +8963,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007a0353840b23e0c6dc73e5b962ff58ed7f6bc9ceff3ce7fe6fbad8d496edf4" dependencies = [ "strum 0.22.0", - "windows", + "windows 0.24.0", "xml-rs", ] [[package]] name = "wry" -version = "0.12.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9549393a3917b5303277abb0267f8eecf9fd629b25f1c04e5284aa58b61915" +checksum = "194b2750d8fe10fef189af5e2ca09e56cb8c5458a365d2b32842b024351f58c9" dependencies = [ "cocoa", - "core-graphics 0.22.3", + "core-graphics", "gdk", "gio", "glib", @@ -8865,14 +8987,15 @@ dependencies = [ "once_cell", "serde 1.0.136", "serde_json", + "sys-info", "tao", "thiserror", "url 2.2.2", "webkit2gtk", "webkit2gtk-sys", - "webview2", - "webview2-sys", - "winapi 0.3.9", + "webview2-com", + "windows 0.30.0", + "windows_macros", ] [[package]] @@ -8885,6 +9008,16 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "x11" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "x11-dl" version = "2.19.1" @@ -9043,18 +9176,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.9.2+zstd.1.5.1" +version = "0.10.0+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "4.1.3+zstd.1.5.1" +version = "4.1.4+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" dependencies = [ "libc", "zstd-sys", @@ -9062,9 +9195,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" +version = "1.6.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" dependencies = [ "cc", "libc", diff --git a/applications/launchpad/backend/src/commands/create_workspace.rs b/applications/launchpad/backend/src/commands/create_workspace.rs index 95b6a6cac3..5930642876 100644 --- a/applications/launchpad/backend/src/commands/create_workspace.rs +++ b/applications/launchpad/backend/src/commands/create_workspace.rs @@ -56,7 +56,13 @@ pub fn copy_config_file>( file: &str, ) -> Result<(), LauncherError> { let path = Path::new("assets").join(file); - let config_path = resolve_path(config, package_info, &path, Some(BaseDirectory::Resource))?; + let config_path = resolve_path( + config, + package_info, + &Default::default(), + &path, + Some(BaseDirectory::Resource), + )?; let cfg = std::fs::read_to_string(&config_path).expect("The config assets were not bundled with the App"); info!("Log Configuration file ({}) loaded", file); debug!("{}", cfg); diff --git a/applications/launchpad/backend/src/main.rs b/applications/launchpad/backend/src/main.rs index 1622ab7ccc..a7e501f5a4 100644 --- a/applications/launchpad/backend/src/main.rs +++ b/applications/launchpad/backend/src/main.rs @@ -14,15 +14,17 @@ use tari_launchpad::{ commands::*, docker::{DockerWrapper, Workspaces}, }; -use tauri::{api::cli::get_matches, async_runtime::block_on, utils::config::CliConfig, Event, Manager}; +use tauri::{api::cli::get_matches, async_runtime::block_on, utils::config::CliConfig, Manager, PackageInfo, RunEvent}; fn main() { env_logger::init(); let context = tauri::generate_context!(); let cli_config = context.config().tauri.cli.clone().unwrap(); + // We're going to attach this to the AppState because Tauri does not expose it for some reason + let package_info = context.package_info().clone(); // Handle --help and --version. Exits after printing - handle_cli_options(&cli_config); + handle_cli_options(&cli_config, &package_info); let docker = match DockerWrapper::new() { Ok(docker) => docker, @@ -34,8 +36,6 @@ fn main() { // TODO - Load workspace definitions from persistent storage here let workspaces = Workspaces::default(); - // We're going to attach this to the AppState because Tauri does not expose it for some reason - let package_info = context.package_info().clone(); info!("Using Docker version: {}", docker.version()); let app = tauri::Builder::default() @@ -55,7 +55,7 @@ fn main() { .expect("error while running Launchpad"); app.run(|app, event| { - if let Event::Exit = event { + if let RunEvent::Exit = event { info!("Received Exit event"); block_on(async move { let state = app.state(); @@ -65,8 +65,8 @@ fn main() { }); } -fn handle_cli_options(cli_config: &CliConfig) { - match get_matches(cli_config) { +fn handle_cli_options(cli_config: &CliConfig, pkg_info: &PackageInfo) { + match get_matches(cli_config, pkg_info) { Ok(matches) => { if let Some(arg_data) = matches.args.get("help") { debug!("{}", arg_data.value.as_str().unwrap_or("No help available")); diff --git a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs index 57f2b5d6d0..baaf51710c 100644 --- a/applications/tari_base_node/src/grpc/base_node_grpc_server.rs +++ b/applications/tari_base_node/src/grpc/base_node_grpc_server.rs @@ -602,8 +602,8 @@ impl tari_rpc::base_node_server::BaseNode for BaseNodeGrpcServer { let response = tari_rpc::ListAssetRegistrationsResponse { asset_public_key: output .features - .mint_non_fungible - .map(|mint| mint.asset_public_key.to_vec()) + .asset + .map(|asset| asset.public_key.to_vec()) .unwrap_or_default(), unique_id: output.features.unique_id.unwrap_or_default(), owner_commitment: output.commitment.to_vec(), diff --git a/applications/tari_base_node/src/main.rs b/applications/tari_base_node/src/main.rs index 7ed6bee243..f877df47d7 100644 --- a/applications/tari_base_node/src/main.rs +++ b/applications/tari_base_node/src/main.rs @@ -357,7 +357,7 @@ async fn run_grpc( .serve_with_shutdown(grpc_address, interrupt_signal.map(|_| ())) .await .map_err(|err| { - error!(target: LOG_TARGET, "GRPC encountered an error:{}", err); + error!(target: LOG_TARGET, "GRPC encountered an error: {}", err); err })?; diff --git a/applications/tari_collectibles/src-tauri/Cargo.toml b/applications/tari_collectibles/src-tauri/Cargo.toml index 383ddf166f..55f8bae803 100644 --- a/applications/tari_collectibles/src-tauri/Cargo.toml +++ b/applications/tari_collectibles/src-tauri/Cargo.toml @@ -7,13 +7,12 @@ license = "MIT" repository = "https://github.com/tari-project/tari" default-run = "tari_collectibles" edition = "2018" -build = "src/build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "1.0.0-beta.4" } +tauri-build = { version = "1.0.0-beta.4", features = [] } [dependencies] tari_app_grpc = { path = "../../tari_app_grpc" } diff --git a/applications/tari_collectibles/src-tauri/src/build.rs b/applications/tari_collectibles/src-tauri/build.rs similarity index 100% rename from applications/tari_collectibles/src-tauri/src/build.rs rename to applications/tari_collectibles/src-tauri/build.rs diff --git a/applications/tari_validator_node/proto/dan/validator_node.proto b/applications/tari_validator_node/proto/dan/validator_node.proto index f43a5982cc..2793847476 100644 --- a/applications/tari_validator_node/proto/dan/validator_node.proto +++ b/applications/tari_validator_node/proto/dan/validator_node.proto @@ -74,7 +74,48 @@ message GetSidechainBlocksRequest { bytes end_hash = 3; } - message GetSidechainBlocksResponse { tari.dan.common.SideChainBlock block = 1; } + +message GetSidechainStateRequest { + bytes asset_public_key = 1; +} + +message GetSidechainStateResponse { + oneof state { + string schema = 1; + KeyValue key_value = 2; + } +} + +message KeyValue { + bytes key = 1; + bytes value = 2; +} + +message GetStateOpLogsRequest { + bytes asset_public_key = 1; + uint64 height = 2; +} + +message GetStateOpLogsResponse { + repeated StateOpLog op_logs = 1; +} + +message StateOpLog { + uint64 height = 1; + string operation = 2; + string schema = 3; + bytes key = 4; + bytes value = 5; + bytes merkle_root = 6; +} + +message GetTipNodeRequest{ + bytes asset_public_key = 1; +} + +message GetTipNodeResponse { + tari.dan.common.Node tip_node = 1; +} diff --git a/applications/tari_validator_node/src/dan_node.rs b/applications/tari_validator_node/src/dan_node.rs index 3ac9cc1380..a7d94ef39d 100644 --- a/applications/tari_validator_node/src/dan_node.rs +++ b/applications/tari_validator_node/src/dan_node.rs @@ -22,7 +22,7 @@ use std::{collections::HashMap, sync::Arc, time::Duration}; -use log::info; +use log::*; use tari_common::{ configuration::ValidatorNodeConfig, exit_codes::{ExitCode, ExitError}, @@ -59,6 +59,7 @@ use crate::{ inbound_connection_service::TariCommsInboundConnectionService, outbound_connection_service::TariCommsOutboundService, }, + TariCommsValidatorNodeClientFactory, }; const LOG_TARGET: &str = "tari::validator_node::app"; @@ -108,16 +109,29 @@ impl DanNode { .get_assets_for_dan_node(node_identity.public_key().clone()) .await .unwrap(); + info!( + target: LOG_TARGET, + "Base node returned {} asset(s) to process", + assets.len() + ); for asset in assets { if tasks.contains_key(&asset.public_key) { + debug!( + target: LOG_TARGET, + "Asset task already running for asset '{}'", asset.public_key + ); continue; } if let Some(allow_list) = &dan_config.assets_allow_list { if !allow_list.contains(&asset.public_key.to_hex()) { + debug!( + target: LOG_TARGET, + "Asset '{}' is not whitelisted for processing ", asset.public_key + ); continue; } } - info!(target: LOG_TARGET, "Adding asset {:?}", asset.public_key); + info!(target: LOG_TARGET, "Adding asset '{}'", asset.public_key); let node_identity = node_identity.as_ref().clone(); let mempool = mempool_service.clone(); let handles = handles.clone(); @@ -128,7 +142,7 @@ impl DanNode { tasks.insert( asset.public_key.clone(), task::spawn(DanNode::start_asset_worker( - asset.clone(), + asset, node_identity, mempool, handles, @@ -199,6 +213,9 @@ impl DanNode { let chain_storage = SqliteStorageService {}; let wallet_client = GrpcWalletClient::new(config.wallet_grpc_address); let checkpoint_manager = ConcreteCheckpointManager::new(asset_definition.clone(), wallet_client); + let connectivity = handles.expect_handle(); + let validator_node_client_factory = + TariCommsValidatorNodeClientFactory::new(connectivity, dht.discovery_service_requester()); let mut consensus_worker = ConsensusWorker::::new( receiver, outbound, @@ -214,6 +231,7 @@ impl DanNode { db_factory, chain_storage, checkpoint_manager, + validator_node_client_factory, ); consensus_worker diff --git a/applications/tari_validator_node/src/grpc/services/base_node_client.rs b/applications/tari_validator_node/src/grpc/services/base_node_client.rs index 236627f353..6fb857dccb 100644 --- a/applications/tari_validator_node/src/grpc/services/base_node_client.rs +++ b/applications/tari_validator_node/src/grpc/services/base_node_client.rs @@ -23,6 +23,7 @@ use std::{convert::TryInto, net::SocketAddr}; use async_trait::async_trait; +use log::*; use tari_app_grpc::tari_rpc as grpc; use tari_common_types::types::PublicKey; use tari_crypto::tari_utilities::ByteArray; @@ -32,6 +33,8 @@ use tari_dan_core::{ DigitalAssetError, }; +const LOG_TARGET: &str = "tari::validator_node::app"; + #[derive(Clone)] pub struct GrpcBaseNodeClient { endpoint: SocketAddr, @@ -112,18 +115,23 @@ impl BaseNodeClient for GrpcBaseNodeClient { self.inner.as_mut().unwrap() }, }; - let request = grpc::ListAssetRegistrationsRequest { offset: 0, count: 0 }; + // TODO: probably should use output mmr indexes here + let request = grpc::ListAssetRegistrationsRequest { offset: 0, count: 100 }; let mut result = inner.list_asset_registrations(request).await.unwrap().into_inner(); let mut assets: Vec = vec![]; let tip = self.get_tip_info().await?; while let Some(r) = result.message().await.unwrap() { - if let Ok(asset_public_key) = PublicKey::from_bytes(r.unique_id.as_bytes()) { + if let Ok(asset_public_key) = PublicKey::from_bytes(r.asset_public_key.as_bytes()) { if let Some(checkpoint) = self .get_current_checkpoint(tip.height_of_longest_chain, asset_public_key.clone(), vec![3u8; 32]) .await? { if let Some(committee) = checkpoint.get_side_chain_committee() { if committee.contains(&dan_node_public_key) { + debug!( + target: LOG_TARGET, + "Node is on committee for asset : {}", asset_public_key + ); assets.push(AssetDefinition { public_key: asset_public_key, template_parameters: r @@ -144,4 +152,32 @@ impl BaseNodeClient for GrpcBaseNodeClient { } Ok(assets) } + + async fn get_asset_registration( + &mut self, + asset_public_key: PublicKey, + ) -> Result, DigitalAssetError> { + let conn = match self.inner.as_mut() { + Some(i) => i, + None => { + self.connect().await?; + self.inner.as_mut().unwrap() + }, + }; + + let req = grpc::GetAssetMetadataRequest { + asset_public_key: asset_public_key.to_vec(), + }; + let output = conn.get_asset_metadata(req).await.unwrap().into_inner(); + + let output = output + .features + .map(|features| match features.try_into() { + Ok(f) => Ok(BaseLayerOutput { features: f }), + Err(e) => Err(DigitalAssetError::ConversionError(e)), + }) + .transpose()?; + + Ok(output) + } } diff --git a/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs b/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs index 0bd54a1a24..f6e126b382 100644 --- a/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs +++ b/applications/tari_validator_node/src/grpc/validator_node_grpc_server.rs @@ -143,10 +143,10 @@ impl rpc::validator_node_ .get_state_db(&asset_public_key) .map_err(|e| Status::internal(format!("Could not create state db: {}", e)))? { - let mut unit_of_work = state.new_unit_of_work(); + let mut state_db_reader = state.reader(); let response_bytes = self .asset_processor - .invoke_read_method(template_id, request.method, &request.args, &mut unit_of_work) + .invoke_read_method(template_id, request.method, &request.args, &mut state_db_reader) .map_err(|e| Status::internal(format!("Could not invoke read method: {}", e)))?; Ok(Response::new(rpc::InvokeReadMethodResponse { result: response_bytes.unwrap_or_default(), diff --git a/applications/tari_validator_node/src/main.rs b/applications/tari_validator_node/src/main.rs index 171cad2916..a6790869ba 100644 --- a/applications/tari_validator_node/src/main.rs +++ b/applications/tari_validator_node/src/main.rs @@ -92,6 +92,10 @@ fn main_inner() -> Result<(), ExitError> { async fn run_node(config: GlobalConfig, create_id: bool) -> Result<(), ExitError> { let shutdown = Shutdown::new(); + let validator_node_config = config + .validator_node + .as_ref() + .ok_or_else(|| ExitError::new(ExitCode::ConfigError, "validator_node configuration not found"))?; fs::create_dir_all(&config.peer_db_path).map_err(|err| ExitError::new(ExitCode::ConfigError, err))?; let node_identity = setup_node_identity( @@ -125,7 +129,7 @@ async fn run_node(config: GlobalConfig, create_id: bool) -> Result<(), ExitError handles.expect_handle::().discovery_service_requester(), ); let asset_proxy: ConcreteAssetProxy = ConcreteAssetProxy::new( - GrpcBaseNodeClient::new(config.validator_node.clone().unwrap().base_node_grpc_address), + GrpcBaseNodeClient::new(validator_node_config.base_node_grpc_address), validator_node_client_factory, 5, mempool_service.clone(), @@ -197,7 +201,7 @@ async fn run_grpc( .serve_with_shutdown(grpc_address, shutdown_signal.map(|_| ())) .await .map_err(|err| { - error!(target: LOG_TARGET, "GRPC encountered an error:{}", err); + error!(target: LOG_TARGET, "GRPC encountered an error: {}", err); err })?; diff --git a/applications/tari_validator_node/src/p2p/proto/conversions.rs b/applications/tari_validator_node/src/p2p/proto/conversions.rs index 1bb4bd7012..115db3db03 100644 --- a/applications/tari_validator_node/src/p2p/proto/conversions.rs +++ b/applications/tari_validator_node/src/p2p/proto/conversions.rs @@ -24,22 +24,27 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::PublicKey; use tari_crypto::tari_utilities::ByteArray; -use tari_dan_core::models::{ - CheckpointData, - HotStuffMessage, - HotStuffMessageType, - HotStuffTreeNode, - Instruction, - InstructionSet, - Node, - QuorumCertificate, - SideChainBlock, - Signature, - StateRoot, - TariDanPayload, - TemplateId, - TreeNodeHash, - ViewId, +use tari_dan_core::{ + models::{ + CheckpointData, + HotStuffMessage, + HotStuffMessageType, + HotStuffTreeNode, + Instruction, + InstructionSet, + KeyValue, + Node, + QuorumCertificate, + SideChainBlock, + Signature, + StateOpLogEntry, + StateRoot, + TariDanPayload, + TemplateId, + TreeNodeHash, + ViewId, + }, + storage::state::DbStateOpLogEntry, }; use crate::p2p::proto; @@ -159,14 +164,19 @@ impl TryFrom for HotStuffTreeNode for Node { Ok(Self::new(hash, parent, height, is_committed)) } } + +impl From for proto::validator_node::KeyValue { + fn from(kv: KeyValue) -> Self { + Self { + key: kv.key, + value: kv.value, + } + } +} + +impl TryFrom for KeyValue { + type Error = String; + + fn try_from(kv: proto::validator_node::KeyValue) -> Result { + if kv.key.is_empty() { + return Err("KeyValue: key cannot be empty".to_string()); + } + + Ok(Self { + key: kv.key, + value: kv.value, + }) + } +} + +impl From for proto::validator_node::StateOpLog { + fn from(entry: StateOpLogEntry) -> Self { + let DbStateOpLogEntry { + height, + merkle_root, + operation, + schema, + key, + value, + } = entry.into_inner(); + Self { + height, + merkle_root: merkle_root.map(|r| r.as_bytes().to_vec()).unwrap_or_default(), + operation: operation.as_op_str().to_string(), + schema, + key, + value: value.unwrap_or_default(), + } + } +} +impl TryFrom for StateOpLogEntry { + type Error = String; + + fn try_from(value: proto::validator_node::StateOpLog) -> Result { + Ok(DbStateOpLogEntry { + height: value.height, + merkle_root: Some(value.merkle_root) + .filter(|r| !r.is_empty()) + .map(TryInto::try_into) + .transpose() + .map_err(|_| "Invalid merkle root value".to_string())?, + operation: value + .operation + .parse() + .map_err(|_| "Invalid oplog operation string".to_string())?, + schema: value.schema, + key: value.key, + value: Some(value.value).filter(|v| !v.is_empty()), + } + .into()) + } +} diff --git a/applications/tari_validator_node/src/p2p/rpc/mod.rs b/applications/tari_validator_node/src/p2p/rpc/mod.rs index e888b36430..771b777e80 100644 --- a/applications/tari_validator_node/src/p2p/rpc/mod.rs +++ b/applications/tari_validator_node/src/p2p/rpc/mod.rs @@ -60,6 +60,24 @@ pub trait ValidatorNodeRpcService: Send + Sync + 'static { &self, request: Request, ) -> Result, RpcStatus>; + + #[rpc(method = 5)] + async fn get_sidechain_state( + &self, + request: Request, + ) -> Result, RpcStatus>; + + #[rpc(method = 6)] + async fn get_op_logs( + &self, + request: Request, + ) -> Result, RpcStatus>; + + #[rpc(method = 7)] + async fn get_tip_node( + &self, + request: Request, + ) -> Result, RpcStatus>; } pub fn create_validator_node_rpc_service< diff --git a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs b/applications/tari_validator_node/src/p2p/rpc/service_impl.rs index 8e6e7fab98..9a7d4947bf 100644 --- a/applications/tari_validator_node/src/p2p/rpc/service_impl.rs +++ b/applications/tari_validator_node/src/p2p/rpc/service_impl.rs @@ -24,12 +24,15 @@ use std::convert::TryFrom; use log::*; use tari_common_types::types::PublicKey; -use tari_comms::protocol::rpc::{Request, Response, RpcStatus, Streaming}; +use tari_comms::{ + protocol::rpc::{Request, Response, RpcStatus, Streaming}, + utils, +}; use tari_crypto::tari_utilities::ByteArray; use tari_dan_core::{ models::{Instruction, TemplateId, TreeNodeHash}, services::{AssetProcessor, MempoolService}, - storage::DbFactory, + storage::{state::StateDbUnitOfWorkReader, DbFactory}, }; use tokio::{sync::mpsc, task}; @@ -82,12 +85,14 @@ where let request = request.into_message(); let asset_public_key = PublicKey::from_bytes(&request.asset_public_key) .map_err(|err| RpcStatus::bad_request(format!("Asset public key was not a valid public key:{}", err)))?; + let state = self .db_factory .get_state_db(&asset_public_key) .map_err(|e| RpcStatus::general(format!("Could not create state db: {}", e)))? .ok_or_else(|| RpcStatus::not_found("This node does not process this asset".to_string()))?; - let mut unit_of_work = state.new_unit_of_work(); + + let mut unit_of_work = state.reader(); let response_bytes = self .asset_processor .invoke_read_method( @@ -97,6 +102,7 @@ where &mut unit_of_work, ) .map_err(|e| RpcStatus::general(format!("Could not invoke read method: {}", e)))?; + Ok(Response::new(proto::InvokeReadMethodResponse { result: response_bytes.unwrap_or_default(), })) @@ -221,4 +227,99 @@ where Ok(Streaming::new(rx)) } + + async fn get_sidechain_state( + &self, + request: Request, + ) -> Result, RpcStatus> { + let msg = request.into_message(); + + let asset_public_key = PublicKey::from_bytes(&msg.asset_public_key) + .map_err(|_| RpcStatus::bad_request("Invalid asset_public_key"))?; + + let db = self + .db_factory + .get_state_db(&asset_public_key) + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| RpcStatus::not_found("Asset not found"))?; + + let uow = db.reader(); + let data = uow.get_all_state().map_err(RpcStatus::log_internal_error(LOG_TARGET))?; + let (tx, rx) = mpsc::channel(10); + + task::spawn(async move { + for state in data { + let schema = proto::GetSidechainStateResponse { + state: Some(proto::get_sidechain_state_response::State::Schema(state.name)), + }; + + if tx.send(Ok(schema)).await.is_err() { + return; + } + + let key_values = state + .items + .into_iter() + .map(|kv| proto::get_sidechain_state_response::State::KeyValue(kv.into())) + .map(|state| Ok(proto::GetSidechainStateResponse { state: Some(state) })); + + if utils::mpsc::send_all(&tx, key_values).await.is_err() { + return; + } + } + }); + + Ok(Streaming::new(rx)) + } + + async fn get_op_logs( + &self, + request: Request, + ) -> Result, RpcStatus> { + let msg = request.into_message(); + + let asset_public_key = PublicKey::from_bytes(&msg.asset_public_key) + .map_err(|_| RpcStatus::bad_request("Invalid asset_public_key"))?; + + let db = self + .db_factory + .get_state_db(&asset_public_key) + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| RpcStatus::not_found("Asset not found"))?; + + let reader = db.reader(); + let op_logs = reader + .get_op_logs_for_height(msg.height) + .map_err(RpcStatus::log_internal_error(LOG_TARGET))?; + + let resp = proto::GetStateOpLogsResponse { + op_logs: op_logs.into_iter().map(Into::into).collect(), + }; + + Ok(Response::new(resp)) + } + + async fn get_tip_node( + &self, + request: Request, + ) -> Result, RpcStatus> { + let msg = request.into_message(); + + let asset_public_key = PublicKey::from_bytes(&msg.asset_public_key) + .map_err(|_| RpcStatus::bad_request("Invalid asset_public_key"))?; + + let db = self + .db_factory + .get_chain_db(&asset_public_key) + .map_err(RpcStatus::log_internal_error(LOG_TARGET))? + .ok_or_else(|| RpcStatus::not_found("Asset not found"))?; + + let tip_node = db.get_tip_node().map_err(RpcStatus::log_internal_error(LOG_TARGET))?; + + let resp = proto::GetTipNodeResponse { + tip_node: tip_node.map(Into::into), + }; + + Ok(Response::new(resp)) + } } diff --git a/applications/tari_validator_node/src/p2p/services/inbound_connection_service.rs b/applications/tari_validator_node/src/p2p/services/inbound_connection_service.rs index 2b48deba66..ba610ac9ce 100644 --- a/applications/tari_validator_node/src/p2p/services/inbound_connection_service.rs +++ b/applications/tari_validator_node/src/p2p/services/inbound_connection_service.rs @@ -116,18 +116,16 @@ impl TariCommsInboundConnectionService { tokio::select! { request = self.request_channel.recv() => { if let Some(request) = request { - self.handle_request(request).await?; - } - else { + self.handle_request(request).await?; + } else { debug!(target: LOG_TARGET, "All requesters have dropped, stopping."); return Ok(()) } }, message = self.loopback_receiver.recv() => { if let Some((from, message)) = message { - self.process_message(from, message).await?; - } - else { + self.process_message(from, message).await?; + } else { debug!(target: LOG_TARGET, "Loopback senders have all dropped, stopping"); return Ok(()) } diff --git a/applications/tari_validator_node/src/p2p/services/rpc_client.rs b/applications/tari_validator_node/src/p2p/services/rpc_client.rs index e009a33c90..3add896ecc 100644 --- a/applications/tari_validator_node/src/p2p/services/rpc_client.rs +++ b/applications/tari_validator_node/src/p2p/services/rpc_client.rs @@ -28,15 +28,14 @@ use tari_common_types::types::PublicKey; use tari_comms::{ connection_manager::ConnectionManagerError, connectivity::{ConnectivityError, ConnectivityRequester}, - peer_manager::NodeId, + peer_manager::{NodeId, PeerManagerError}, PeerConnection, }; use tari_comms_dht::{envelope::NodeDestination, DhtDiscoveryRequester}; use tari_crypto::tari_utilities::ByteArray; use tari_dan_core::{ - models::{SideChainBlock, TemplateId, TreeNodeHash}, - services::{ValidatorNodeClientFactory, ValidatorNodeRpcClient}, - DigitalAssetError, + models::{Node, SchemaState, SideChainBlock, StateOpLogEntry, TemplateId, TreeNodeHash}, + services::{ValidatorNodeClientError, ValidatorNodeClientFactory, ValidatorNodeRpcClient}, }; use tokio_stream::StreamExt; @@ -51,7 +50,7 @@ pub struct TariCommsValidatorNodeRpcClient { } impl TariCommsValidatorNodeRpcClient { - async fn create_connection(&mut self) -> Result { + async fn create_connection(&mut self) -> Result { match self.connectivity.dial_peer(NodeId::from(self.address.clone())).await { Ok(connection) => Ok(connection), Err(connectivity_error) => { @@ -61,7 +60,8 @@ impl TariCommsValidatorNodeRpcClient { match err { ConnectionManagerError::PeerConnectionError(_) | ConnectionManagerError::DialConnectFailedAllAddresses | - ConnectionManagerError::PeerIdentityNoValidAddresses => { + ConnectionManagerError::PeerIdentityNoValidAddresses | + ConnectionManagerError::PeerManagerError(PeerManagerError::PeerNotFoundError) => { // Try discover, then dial again // TODO: Should make discovery and connect the responsibility of the DHT layer self.dht_discovery @@ -97,7 +97,7 @@ impl ValidatorNodeRpcClient for TariCommsValidatorNodeRpcClient { template_id: TemplateId, method: String, args: Vec, - ) -> Result>, DigitalAssetError> { + ) -> Result>, ValidatorNodeClientError> { debug!( target: LOG_TARGET, r#"Invoking read method "{}" for asset '{}'"#, method, asset_public_key @@ -125,7 +125,7 @@ impl ValidatorNodeRpcClient for TariCommsValidatorNodeRpcClient { template_id: TemplateId, method: String, args: Vec, - ) -> Result>, DigitalAssetError> { + ) -> Result>, ValidatorNodeClientError> { debug!( target: LOG_TARGET, r#"Invoking method "{}" for asset '{}'"#, method, asset_public_key @@ -156,7 +156,7 @@ impl ValidatorNodeRpcClient for TariCommsValidatorNodeRpcClient { asset_public_key: &PublicKey, start_hash: TreeNodeHash, end_hash: Option, - ) -> Result, DigitalAssetError> { + ) -> Result, ValidatorNodeClientError> { let mut connection = self.create_connection().await?; let mut client = connection.connect_rpc::().await?; let request = proto::GetSidechainBlocksRequest { @@ -168,22 +168,114 @@ impl ValidatorNodeRpcClient for TariCommsValidatorNodeRpcClient { let stream = client.get_sidechain_blocks(request).await?; // TODO: By first collecting all the blocks, we lose the advantage of streaming. Since you cannot return // `Result, _>`, and the Map type is private in tokio-stream, its a little tricky to - // return the stream and not leak the RPC response type out of the client + // return the stream and not leak the RPC response type out of the client. + // Copying the tokio_stream::Map stream into our code / creating a custom conversion stream wrapper would + // solve this. let blocks = stream .map(|result| { - let resp = result.map_err(DigitalAssetError::from)?; + let resp = result?; let block: SideChainBlock = resp .block - .ok_or_else(|| DigitalAssetError::ConversionError("Node returned empty block".to_string()))? + .ok_or_else(|| { + ValidatorNodeClientError::InvalidPeerMessage("Node returned empty block".to_string()) + })? .try_into() - .map_err(DigitalAssetError::ConversionError)?; + .map_err(ValidatorNodeClientError::InvalidPeerMessage)?; Ok(block) }) - .collect::>() + .collect::>() .await?; Ok(blocks) } + + async fn get_sidechain_state( + &mut self, + asset_public_key: &PublicKey, + ) -> Result, ValidatorNodeClientError> { + let mut connection = self.create_connection().await?; + let mut client = connection.connect_rpc::().await?; + let request = proto::GetSidechainStateRequest { + asset_public_key: asset_public_key.to_vec(), + }; + + let mut stream = client.get_sidechain_state(request).await?; + // TODO: Same issue as get_sidechain_blocks + let mut schemas = Vec::new(); + let mut current_schema = None; + while let Some(resp) = stream.next().await { + let resp = resp?; + + match resp.state { + Some(proto::get_sidechain_state_response::State::Schema(name)) => { + if let Some(schema) = current_schema.take() { + schemas.push(schema); + } + current_schema = Some(SchemaState::new(name, vec![])); + }, + Some(proto::get_sidechain_state_response::State::KeyValue(kv)) => match current_schema.as_mut() { + Some(schema) => { + let kv = kv.try_into().map_err(ValidatorNodeClientError::InvalidPeerMessage)?; + schema.push_key_value(kv); + }, + None => { + return Err(ValidatorNodeClientError::InvalidPeerMessage(format!( + "Peer {} sent a key value response without first defining the schema", + self.address + ))) + }, + }, + None => { + return Err(ValidatorNodeClientError::ProtocolViolation { + peer: self.address.clone(), + details: "get_sidechain_state: Peer sent response without state".to_string(), + }) + }, + } + } + + if let Some(schema) = current_schema { + schemas.push(schema); + } + + Ok(schemas) + } + + async fn get_op_logs( + &mut self, + asset_public_key: &PublicKey, + height: u64, + ) -> Result, ValidatorNodeClientError> { + let mut connection = self.create_connection().await?; + let mut client = connection.connect_rpc::().await?; + let request = proto::GetStateOpLogsRequest { + asset_public_key: asset_public_key.as_bytes().to_vec(), + height, + }; + + let resp = client.get_op_logs(request).await?; + let op_logs = resp + .op_logs + .into_iter() + .map(TryInto::try_into) + .collect::, _>>() + .map_err(ValidatorNodeClientError::InvalidPeerMessage)?; + + Ok(op_logs) + } + + async fn get_tip_node(&mut self, asset_public_key: &PublicKey) -> Result, ValidatorNodeClientError> { + let mut connection = self.create_connection().await?; + let mut client = connection.connect_rpc::().await?; + let request = proto::GetTipNodeRequest { + asset_public_key: asset_public_key.as_bytes().to_vec(), + }; + let resp = client.get_tip_node(request).await?; + resp.tip_node + .map(TryInto::try_into) + .transpose() + .map_err(ValidatorNodeClientError::InvalidPeerMessage) + } } #[derive(Clone)] diff --git a/common/src/configuration/validator_node_config.rs b/common/src/configuration/validator_node_config.rs index 79c9eba215..6ca54043b8 100644 --- a/common/src/configuration/validator_node_config.rs +++ b/common/src/configuration/validator_node_config.rs @@ -25,7 +25,7 @@ use std::{ path::PathBuf, }; -use config::Config; +use config::{Config, ConfigError}; use serde::Deserialize; use crate::ConfigurationError; @@ -62,10 +62,12 @@ impl ValidatorNodeConfig { pub fn convert_if_present(cfg: Config) -> Result, ConfigurationError> { let section: Self = match cfg.get("validator_node") { Ok(s) => s, - Err(_e) => { - // dbg!(e); + Err(ConfigError::NotFound(_)) => { return Ok(None); }, + Err(err) => { + return Err(err.into()); + }, }; Ok(Some(section)) // dbg!(§ion); diff --git a/dan_layer/common_types/proto/tips/tip002.proto b/dan_layer/common_types/proto/tips/tip002.proto index ff5f621bd1..64a02c5e98 100644 --- a/dan_layer/common_types/proto/tips/tip002.proto +++ b/dan_layer/common_types/proto/tips/tip002.proto @@ -7,7 +7,7 @@ service Tip002 { rpc Init(InitRequest) returns (Empty); // rpc Info(InfoRequest) returns (InfoResponse); rpc BalanceOf(BalanceOfRequest) returns (BalanceOfResponse); - rpc Tranfer(TransferRequest) returns (TransferResponse); + rpc Transfer(TransferRequest) returns (TransferResponse); } @@ -47,4 +47,4 @@ message BalanceOfRequest { message BalanceOfResponse { uint64 balance = 1; -} \ No newline at end of file +} diff --git a/dan_layer/core/Cargo.toml b/dan_layer/core/Cargo.toml index 59c417fb27..fa3c69eb8e 100644 --- a/dan_layer/core/Cargo.toml +++ b/dan_layer/core/Cargo.toml @@ -31,6 +31,7 @@ log = { version = "0.4.8", features = ["std"] } lmdb-zero = "0.4.4" prost = "0.9" prost-types = "0.9" +rand = "0.8.4" serde = "1.0.126" thiserror = "^1.0.20" tokio = { version="1.10", features = ["macros", "time"]} diff --git a/dan_layer/core/src/digital_assets_error.rs b/dan_layer/core/src/digital_assets_error.rs index 470958b557..65fcf6d072 100644 --- a/dan_layer/core/src/digital_assets_error.rs +++ b/dan_layer/core/src/digital_assets_error.rs @@ -20,14 +20,9 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use tari_comms::{ - connectivity::ConnectivityError, - protocol::rpc::{RpcError, RpcStatus}, -}; -use tari_comms_dht::DhtDiscoveryError; use thiserror::Error; -use crate::{models::ModelError, storage::StorageError}; +use crate::{models::ModelError, services::ValidatorNodeClientError, storage::StorageError, workers::StateSyncError}; #[derive(Debug, Error)] pub enum DigitalAssetError { @@ -71,18 +66,16 @@ pub enum DigitalAssetError { NoCommitteeForAsset, #[error("None of the committee responded")] NoResponsesFromCommittee, - #[error("Connectivity error:{0}")] - ConnectivityError(#[from] ConnectivityError), - #[error("RpcError: {0}")] - RpcError(#[from] RpcError), - #[error("Remote node returned error: {0}")] - RpcStatusError(#[from] RpcStatus), - #[error("Dht Discovery error: {0}")] - DhtDiscoveryError(#[from] DhtDiscoveryError), #[error("Fatal error: {0}")] FatalError(String), #[error(transparent)] ModelError(#[from] ModelError), + #[error("UTXO missing checkpoint data")] + UtxoNoCheckpointData, + #[error("Failed to synchronize state: {0}")] + StateSyncError(#[from] StateSyncError), + #[error("Validator node client error: {0}")] + ValidatorNodeClientError(#[from] ValidatorNodeClientError), } impl From for DigitalAssetError { diff --git a/dan_layer/core/src/fixed_hash.rs b/dan_layer/core/src/fixed_hash.rs index 66281ab5b9..ea642e2c3f 100644 --- a/dan_layer/core/src/fixed_hash.rs +++ b/dan_layer/core/src/fixed_hash.rs @@ -31,7 +31,7 @@ const ZERO_HASH: [u8; FixedHash::byte_size()] = [0u8; FixedHash::byte_size()]; #[error("Invalid size")] pub struct FixedHashSizeError; -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] pub struct FixedHash([u8; FixedHash::byte_size()]); impl FixedHash { @@ -50,7 +50,7 @@ impl FixedHash { impl From<[u8; FixedHash::byte_size()]> for FixedHash { fn from(hash: [u8; FixedHash::byte_size()]) -> Self { - hash.into() + Self(hash) } } @@ -58,12 +58,20 @@ impl TryFrom> for FixedHash { type Error = FixedHashSizeError; fn try_from(value: Vec) -> Result { - if value.len() != FixedHash::byte_size() { + TryFrom::try_from(value.as_slice()) + } +} + +impl TryFrom<&[u8]> for FixedHash { + type Error = FixedHashSizeError; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != FixedHash::byte_size() { return Err(FixedHashSizeError); } let mut buf = [0u8; FixedHash::byte_size()]; - buf.copy_from_slice(&value); + buf.copy_from_slice(bytes); Ok(Self(buf)) } } diff --git a/dan_layer/core/src/models/asset_definition.rs b/dan_layer/core/src/models/asset_definition.rs index 46482ee4f8..5896e09c88 100644 --- a/dan_layer/core/src/models/asset_definition.rs +++ b/dan_layer/core/src/models/asset_definition.rs @@ -94,6 +94,17 @@ pub struct SchemaState { pub items: Vec, } +impl SchemaState { + pub fn new(name: String, items: Vec) -> Self { + Self { name, items } + } + + pub fn push_key_value(&mut self, key_value: KeyValue) -> &mut Self { + self.items.push(key_value); + self + } +} + #[derive(Serialize, Deserialize, Default, Clone, Debug)] pub struct KeyValue { pub key: Vec, diff --git a/dan_layer/core/src/models/base_layer_output.rs b/dan_layer/core/src/models/base_layer_output.rs index 35a3fed8ce..99055828ac 100644 --- a/dan_layer/core/src/models/base_layer_output.rs +++ b/dan_layer/core/src/models/base_layer_output.rs @@ -21,9 +21,14 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. //! A trait to allow abstraction from a specific base layer output +use std::convert::TryFrom; + use tari_common_types::types::PublicKey; -use tari_core::transactions::transaction_components::OutputFeatures; +use tari_core::transactions::transaction_components::{OutputFeatures, OutputFlags}; + +use crate::{fixed_hash::FixedHash, models::ModelError}; +#[derive(Debug)] pub struct BaseLayerOutput { pub features: OutputFeatures, } @@ -35,4 +40,54 @@ impl BaseLayerOutput { .as_ref() .map(|s| s.committee.as_slice()) } + + pub fn get_checkpoint_merkle_root(&self) -> Option { + self.features + .sidechain_checkpoint + .as_ref() + .map(|cp| cp.merkle_root.into()) + } + + pub fn get_parent_public_key(&self) -> Option<&PublicKey> { + self.features.parent_public_key.as_ref() + } +} + +#[derive(Debug, Clone)] +pub struct CheckpointOutput { + pub flags: OutputFlags, + pub parent_public_key: PublicKey, + pub merkle_root: FixedHash, + pub committee: Vec, +} + +impl TryFrom for CheckpointOutput { + type Error = ModelError; + + fn try_from(output: BaseLayerOutput) -> Result { + if !output.features.flags.contains(OutputFlags::SIDECHAIN_CHECKPOINT) { + return Err(ModelError::NotCheckpointOutput); + } + + let parent_public_key = output + .get_parent_public_key() + .cloned() + .ok_or(ModelError::CheckpointOutputMissingParentPublicKey)?; + + let merkle_root = output + .get_checkpoint_merkle_root() + .ok_or(ModelError::CheckpointOutputMissingCheckpointMerkleRoot)?; + + let committee = output + .get_side_chain_committee() + .ok_or(ModelError::CheckpointOutputMissingSidechainCommittee)? + .to_vec(); + + Ok(Self { + flags: output.features.flags, + parent_public_key, + merkle_root, + committee, + }) + } } diff --git a/dan_layer/core/src/models/domain_events/consensus_worker_domain_event.rs b/dan_layer/core/src/models/domain_events/consensus_worker_domain_event.rs index 978e4a5c95..abeb9def29 100644 --- a/dan_layer/core/src/models/domain_events/consensus_worker_domain_event.rs +++ b/dan_layer/core/src/models/domain_events/consensus_worker_domain_event.rs @@ -27,8 +27,8 @@ use crate::models::{ConsensusWorkerState, Event}; #[derive(Debug, Clone, PartialEq)] pub enum ConsensusWorkerDomainEvent { StateChanged { - old: ConsensusWorkerState, - new: ConsensusWorkerState, + from: ConsensusWorkerState, + to: ConsensusWorkerState, }, } @@ -37,7 +37,7 @@ impl Event for ConsensusWorkerDomainEvent {} impl fmt::Display for ConsensusWorkerDomainEvent { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - ConsensusWorkerDomainEvent::StateChanged { old, new } => { + ConsensusWorkerDomainEvent::StateChanged { from: old, to: new } => { write!(f, "State changed from {:?} to {:?}", old, new) }, } diff --git a/dan_layer/core/src/models/error.rs b/dan_layer/core/src/models/error.rs index d0fd37ec53..c6ffc8b6fa 100644 --- a/dan_layer/core/src/models/error.rs +++ b/dan_layer/core/src/models/error.rs @@ -26,4 +26,12 @@ pub enum ModelError { InvalidTemplateIdNumber { value: i64 }, #[error("Failed to parse string: {details}")] StringParseError { details: String }, + #[error("Checkpoint output is missing parent public key")] + CheckpointOutputMissingParentPublicKey, + #[error("Checkpoint output is missing checkpoint merkle root")] + CheckpointOutputMissingCheckpointMerkleRoot, + #[error("Checkpoint output is missing side chain committee")] + CheckpointOutputMissingSidechainCommittee, + #[error("Output is not flagged as a checkpoint output")] + NotCheckpointOutput, } diff --git a/dan_layer/core/src/models/hot_stuff_tree_node.rs b/dan_layer/core/src/models/hot_stuff_tree_node.rs index 9cc9543e3b..66458c8a58 100644 --- a/dan_layer/core/src/models/hot_stuff_tree_node.rs +++ b/dan_layer/core/src/models/hot_stuff_tree_node.rs @@ -52,7 +52,7 @@ impl HotStuffTreeNode { parent: TreeNodeHash::zero(), payload, hash: TreeNodeHash::zero(), - state_root: StateRoot::default(), + state_root: StateRoot::initial(), height: 0, }; s.hash = s.calculate_hash(); diff --git a/dan_layer/core/src/models/mod.rs b/dan_layer/core/src/models/mod.rs index 007452bdef..7f6f0c870e 100644 --- a/dan_layer/core/src/models/mod.rs +++ b/dan_layer/core/src/models/mod.rs @@ -38,6 +38,7 @@ mod hot_stuff_tree_node; mod instruction; mod instruction_set; mod node; +mod op_log; mod payload; mod quorum_certificate; mod sidechain_block; @@ -48,9 +49,9 @@ mod tree_node_hash; mod view; mod view_id; -pub use asset_definition::AssetDefinition; +pub use asset_definition::{AssetDefinition, InitialState, KeyValue, SchemaState}; pub use base_layer_metadata::BaseLayerMetadata; -pub use base_layer_output::BaseLayerOutput; +pub use base_layer_output::{BaseLayerOutput, CheckpointOutput}; pub use committee::Committee; pub use error::ModelError; pub use hot_stuff_message::HotStuffMessage; @@ -58,6 +59,7 @@ pub use hot_stuff_tree_node::HotStuffTreeNode; pub use instruction::Instruction; pub use instruction_set::InstructionSet; pub use node::Node; +pub use op_log::{StateOpLogEntry, StateOperation}; pub use payload::Payload; pub use quorum_certificate::QuorumCertificate; pub use sidechain_block::SideChainBlock; @@ -211,6 +213,7 @@ pub trait Event: Clone + Send + Sync {} #[derive(Debug, Clone, Copy, PartialEq)] pub enum ConsensusWorkerState { Starting, + Synchronizing, Prepare, PreCommit, Commit, diff --git a/dan_layer/core/src/models/op_log.rs b/dan_layer/core/src/models/op_log.rs new file mode 100644 index 0000000000..0479a7b442 --- /dev/null +++ b/dan_layer/core/src/models/op_log.rs @@ -0,0 +1,69 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::storage::state::{DbStateOpLogEntry, DbStateOperation}; + +#[derive(Debug)] +pub struct StateOpLogEntry { + inner: DbStateOpLogEntry, +} + +impl StateOpLogEntry { + pub fn operation(&self) -> StateOperation { + self.inner.operation.into() + } + + pub fn into_inner(self) -> DbStateOpLogEntry { + self.inner + } +} + +impl From for StateOpLogEntry { + fn from(inner: DbStateOpLogEntry) -> Self { + Self { inner } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum StateOperation { + Set, + Delete, +} + +impl StateOperation { + pub fn as_op_str(&self) -> &str { + use StateOperation::*; + match self { + Set => "S", + Delete => "D", + } + } +} + +impl From for StateOperation { + fn from(op: DbStateOperation) -> Self { + match op { + DbStateOperation::Set => StateOperation::Set, + DbStateOperation::Delete => StateOperation::Delete, + } + } +} diff --git a/dan_layer/core/src/models/state_root.rs b/dan_layer/core/src/models/state_root.rs index 801e42b888..a16ce23caa 100644 --- a/dan_layer/core/src/models/state_root.rs +++ b/dan_layer/core/src/models/state_root.rs @@ -20,17 +20,25 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#[derive(Default, PartialEq, Debug, Clone)] +use crate::fixed_hash::FixedHash; + +#[derive(PartialEq, Debug, Clone)] pub struct StateRoot { - root: Vec, + root: FixedHash, } impl StateRoot { - pub fn new(root: Vec) -> Self { + pub fn new(root: FixedHash) -> Self { Self { root } } pub fn as_bytes(&self) -> &[u8] { self.root.as_slice() } + + pub fn initial() -> Self { + Self { + root: FixedHash::zero(), + } + } } diff --git a/dan_layer/core/src/models/tari_dan_payload.rs b/dan_layer/core/src/models/tari_dan_payload.rs index 167793e9bf..f04d59012b 100644 --- a/dan_layer/core/src/models/tari_dan_payload.rs +++ b/dan_layer/core/src/models/tari_dan_payload.rs @@ -25,11 +25,14 @@ use std::fmt::Debug; use digest::Digest; use tari_crypto::common::Blake256; -use crate::models::{ConsensusHash, Instruction, InstructionSet, Payload}; +use crate::{ + fixed_hash::FixedHash, + models::{ConsensusHash, Instruction, InstructionSet, Payload}, +}; #[derive(Debug, Clone)] pub struct TariDanPayload { - hash: Vec, + hash: FixedHash, instruction_set: InstructionSet, checkpoint: Option, } @@ -37,7 +40,7 @@ pub struct TariDanPayload { impl TariDanPayload { pub fn new(instruction_set: InstructionSet, checkpoint: Option) -> Self { let mut result = Self { - hash: vec![], + hash: FixedHash::zero(), instruction_set, checkpoint, }; @@ -53,12 +56,12 @@ impl TariDanPayload { self.instruction_set.instructions() } - fn calculate_hash(&self) -> Vec { + fn calculate_hash(&self) -> FixedHash { let result = Blake256::new().chain(self.instruction_set.consensus_hash()); if let Some(ref ck) = self.checkpoint { - result.chain(ck.consensus_hash()).finalize().to_vec() + result.chain(ck.consensus_hash()).finalize().into() } else { - result.finalize().to_vec() + result.finalize().into() } } } @@ -73,7 +76,7 @@ impl Payload for TariDanPayload {} #[derive(Debug, Clone, Default)] pub struct CheckpointData { - hash: Vec, + hash: FixedHash, } impl ConsensusHash for CheckpointData { diff --git a/dan_layer/core/src/services/asset_processor.rs b/dan_layer/core/src/services/asset_processor.rs index dac631f9da..8d15ebd3ca 100644 --- a/dan_layer/core/src/services/asset_processor.rs +++ b/dan_layer/core/src/services/asset_processor.rs @@ -27,7 +27,7 @@ use tari_core::transactions::transaction_components::TemplateParameter; use crate::{ digital_assets_error::DigitalAssetError, models::{AssetDefinition, Instruction, TemplateId}, - storage::state::StateDbUnitOfWork, + storage::state::{StateDbUnitOfWork, StateDbUnitOfWorkReader}, template_command::ExecutionResult, templates::{tip002_template, tip004_template, tip721_template}, }; @@ -48,12 +48,12 @@ pub trait AssetProcessor: Sync + Send + 'static { db: &mut TUnitOfWork, ) -> Result<(), DigitalAssetError>; - fn invoke_read_method( + fn invoke_read_method( &self, template_id: TemplateId, method: String, args: &[u8], - state_db: &mut TUnifOfWork, + state_db: &mut TUnitOfWorkReader, ) -> Result>, DigitalAssetError>; } @@ -89,12 +89,12 @@ impl AssetProcessor for ConcreteAssetProcessor { ) } - fn invoke_read_method( + fn invoke_read_method( &self, template_id: TemplateId, method: String, args: &[u8], - state_db: &mut TUnifOfWork, + state_db: &mut TUnitOfWork, ) -> Result>, DigitalAssetError> { match template_id { TemplateId::Tip002 => tip002_template::invoke_read_method(method, args, state_db), diff --git a/dan_layer/core/src/services/asset_proxy.rs b/dan_layer/core/src/services/asset_proxy.rs index f7b548c2d3..cb4890f7ae 100644 --- a/dan_layer/core/src/services/asset_proxy.rs +++ b/dan_layer/core/src/services/asset_proxy.rs @@ -101,9 +101,10 @@ impl> ConcreteAsse args: Vec, ) -> Result>, DigitalAssetError> { let mut client = self.validator_node_client_factory.create_client(member); - client + let resp = client .invoke_read_method(asset_public_key, template_id, method, args) - .await + .await?; + Ok(resp) } async fn forward_invoke_to_node( @@ -115,7 +116,10 @@ impl> ConcreteAsse args: Vec, ) -> Result>, DigitalAssetError> { let mut client = self.validator_node_client_factory.create_client(member); - client.invoke_method(asset_public_key, template_id, method, args).await + let resp = client + .invoke_method(asset_public_key, template_id, method, args) + .await?; + Ok(resp) } #[allow(clippy::for_loops_over_fallibles)] diff --git a/dan_layer/core/src/services/base_node_client.rs b/dan_layer/core/src/services/base_node_client.rs index ae77144a8d..2afbacab4d 100644 --- a/dan_layer/core/src/services/base_node_client.rs +++ b/dan_layer/core/src/services/base_node_client.rs @@ -43,4 +43,9 @@ pub trait BaseNodeClient { &mut self, dan_node_public_key: PublicKey, ) -> Result, DigitalAssetError>; + + async fn get_asset_registration( + &mut self, + asset_public_key: PublicKey, + ) -> Result, DigitalAssetError>; } diff --git a/dan_layer/core/src/services/events_publisher.rs b/dan_layer/core/src/services/events_publisher.rs index 73da7dcd08..866a5af2cb 100644 --- a/dan_layer/core/src/services/events_publisher.rs +++ b/dan_layer/core/src/services/events_publisher.rs @@ -48,6 +48,6 @@ impl Default for LoggingEventsPublisher { impl EventsPublisher for LoggingEventsPublisher { fn publish(&mut self, event: TEvent) { - debug!(target: LOG_TARGET, "[Event] Event received:{}", event); + debug!(target: LOG_TARGET, "[Event] Event received: {}", event); } } diff --git a/dan_layer/core/src/services/mocks/mod.rs b/dan_layer/core/src/services/mocks/mod.rs index c701602523..36db7254da 100644 --- a/dan_layer/core/src/services/mocks/mod.rs +++ b/dan_layer/core/src/services/mocks/mod.rs @@ -57,7 +57,7 @@ use crate::{ PayloadProvider, SigningService, }, - storage::state::StateDbUnitOfWork, + storage::state::{StateDbUnitOfWork, StateDbUnitOfWorkReader}, }; #[derive(Debug, Clone)] @@ -207,6 +207,13 @@ impl BaseNodeClient for MockBaseNodeClient { ) -> Result, DigitalAssetError> { todo!(); } + + async fn get_asset_registration( + &mut self, + _asset_public_key: PublicKey, + ) -> Result, DigitalAssetError> { + todo!() + } } pub fn mock_base_node_client() -> MockBaseNodeClient { @@ -289,7 +296,7 @@ impl AssetProcessor for MockAssetProcessor { todo!() } - fn invoke_read_method( + fn invoke_read_method( &self, _template_id: TemplateId, _method: String, diff --git a/dan_layer/core/src/services/mod.rs b/dan_layer/core/src/services/mod.rs index 57ef78dbb9..34a809df4a 100644 --- a/dan_layer/core/src/services/mod.rs +++ b/dan_layer/core/src/services/mod.rs @@ -48,5 +48,5 @@ mod validator_node_rpc_client; mod wallet_client; pub use checkpoint_manager::{CheckpointManager, ConcreteCheckpointManager}; pub use service_specification::ServiceSpecification; -pub use validator_node_rpc_client::{ValidatorNodeClientFactory, ValidatorNodeRpcClient}; +pub use validator_node_rpc_client::{ValidatorNodeClientError, ValidatorNodeClientFactory, ValidatorNodeRpcClient}; pub use wallet_client::WalletClient; diff --git a/dan_layer/core/src/services/validator_node_rpc_client.rs b/dan_layer/core/src/services/validator_node_rpc_client.rs index 9392fcb730..672b29351d 100644 --- a/dan_layer/core/src/services/validator_node_rpc_client.rs +++ b/dan_layer/core/src/services/validator_node_rpc_client.rs @@ -22,11 +22,16 @@ use async_trait::async_trait; use tari_common_types::types::PublicKey; +use tari_comms::{ + connectivity::ConnectivityError, + protocol::rpc::{RpcError, RpcStatus}, + types::CommsPublicKey, +}; +use tari_comms_dht::DhtDiscoveryError; use crate::{ - models::{SideChainBlock, TemplateId, TreeNodeHash}, + models::{Node, SchemaState, SideChainBlock, StateOpLogEntry, TemplateId, TreeNodeHash}, services::infrastructure_services::NodeAddressable, - DigitalAssetError, }; pub trait ValidatorNodeClientFactory { @@ -43,7 +48,7 @@ pub trait ValidatorNodeRpcClient { template_id: TemplateId, method: String, args: Vec, - ) -> Result>, DigitalAssetError>; + ) -> Result>, ValidatorNodeClientError>; async fn invoke_method( &mut self, @@ -51,12 +56,41 @@ pub trait ValidatorNodeRpcClient { template_id: TemplateId, method: String, args: Vec, - ) -> Result>, DigitalAssetError>; + ) -> Result>, ValidatorNodeClientError>; async fn get_sidechain_blocks( &mut self, asset_public_key: &PublicKey, start_hash: TreeNodeHash, end_hash: Option, - ) -> Result, DigitalAssetError>; + ) -> Result, ValidatorNodeClientError>; + + async fn get_sidechain_state( + &mut self, + asset_public_key: &PublicKey, + ) -> Result, ValidatorNodeClientError>; + + async fn get_op_logs( + &mut self, + asset_public_key: &PublicKey, + height: u64, + ) -> Result, ValidatorNodeClientError>; + + async fn get_tip_node(&mut self, asset_public_key: &PublicKey) -> Result, ValidatorNodeClientError>; +} + +#[derive(Debug, thiserror::Error)] +pub enum ValidatorNodeClientError { + #[error("Protocol violations for peer {peer}: {details}")] + ProtocolViolation { peer: CommsPublicKey, details: String }, + #[error("Peer sent an invalid message: {0}")] + InvalidPeerMessage(String), + #[error("Connectivity error:{0}")] + ConnectivityError(#[from] ConnectivityError), + #[error("RpcError: {0}")] + RpcError(#[from] RpcError), + #[error("Remote node returned error: {0}")] + RpcStatusError(#[from] RpcStatus), + #[error("Dht Discovery error: {0}")] + DhtDiscoveryError(#[from] DhtDiscoveryError), } diff --git a/dan_layer/core/src/storage/chain/chain_db.rs b/dan_layer/core/src/storage/chain/chain_db.rs index 866591974f..3e85921a62 100644 --- a/dan_layer/core/src/storage/chain/chain_db.rs +++ b/dan_layer/core/src/storage/chain/chain_db.rs @@ -19,7 +19,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::{ - models::{QuorumCertificate, SideChainBlock, TreeNodeHash}, + models::{Node, QuorumCertificate, SideChainBlock, TreeNodeHash}, storage::{ chain::{chain_db_unit_of_work::ChainDbUnitOfWorkImpl, ChainDbBackendAdapter}, StorageError, @@ -97,6 +97,11 @@ impl ChainDb { Ok(Some(SideChainBlock::new(node.into(), instructions))) } + + pub fn get_tip_node(&self) -> Result, StorageError> { + let db_node = self.adapter.get_tip_node().map_err(TBackendAdapter::Error::into)?; + Ok(db_node.map(Into::into)) + } } impl ChainDb { diff --git a/dan_layer/core/src/storage/chain/chain_db_backend_adapter.rs b/dan_layer/core/src/storage/chain/chain_db_backend_adapter.rs index 70e3846ddf..031a8c96c6 100644 --- a/dan_layer/core/src/storage/chain/chain_db_backend_adapter.rs +++ b/dan_layer/core/src/storage/chain/chain_db_backend_adapter.rs @@ -37,8 +37,9 @@ pub trait ChainDbBackendAdapter: Send + Sync + Clone { type Payload: Payload; fn is_empty(&self) -> Result; - fn node_exists(&self, node_hash: &TreeNodeHash) -> Result; fn create_transaction(&self) -> Result; + fn node_exists(&self, node_hash: &TreeNodeHash) -> Result; + fn get_tip_node(&self) -> Result, Self::Error>; fn insert_node(&self, item: &DbNode, transaction: &Self::BackendTransaction) -> Result<(), Self::Error>; fn update_node( &self, diff --git a/dan_layer/core/src/storage/chain/chain_db_unit_of_work.rs b/dan_layer/core/src/storage/chain/chain_db_unit_of_work.rs index f7cbe41e83..db812fe1df 100644 --- a/dan_layer/core/src/storage/chain/chain_db_unit_of_work.rs +++ b/dan_layer/core/src/storage/chain/chain_db_unit_of_work.rs @@ -27,7 +27,7 @@ use std::{ }; use crate::{ - models::{Instruction, QuorumCertificate, TreeNodeHash}, + models::{Instruction, Node, QuorumCertificate, TreeNodeHash}, storage::{ chain::{db_node::DbNode, ChainDbBackendAdapter, DbInstruction, DbQc}, unit_of_work_tracker::UnitOfWorkTracker, @@ -45,6 +45,7 @@ pub trait ChainDbUnitOfWork: Clone + Send + Sync { fn set_prepare_qc(&mut self, qc: &QuorumCertificate) -> Result<(), StorageError>; fn commit_node(&mut self, node_hash: &TreeNodeHash) -> Result<(), StorageError>; // fn find_proposed_node(&mut self, node_hash: TreeNodeHash) -> Result<(Self::Id, UnitOfWorkTracker), StorageError>; + fn get_tip_node(&self) -> Result, StorageError>; } // Cloneable, Send, Sync wrapper @@ -286,6 +287,11 @@ impl ChainDbUnitOfWork for ChainDbUnitOf node.is_committed = true; Ok(()) } + + fn get_tip_node(&self) -> Result, StorageError> { + let inner = self.inner.read().unwrap(); + inner.get_tip_node() + } } pub struct ChainDbUnitOfWorkInner { @@ -331,4 +337,12 @@ impl ChainDbUnitOfWorkInner Result, StorageError> { + let node = self + .backend_adapter + .get_tip_node() + .map_err(TBackendAdapter::Error::into)?; + Ok(node.map(Into::into)) + } } diff --git a/dan_layer/core/src/storage/mocks/chain_db.rs b/dan_layer/core/src/storage/mocks/chain_db.rs index f2de215bb9..70eeb830a0 100644 --- a/dan_layer/core/src/storage/mocks/chain_db.rs +++ b/dan_layer/core/src/storage/mocks/chain_db.rs @@ -180,4 +180,19 @@ impl ChainDbBackendAdapter for MockChainDbBackupAdapter { lock.locked_qc.update(id, locked_qc.clone()); Ok(()) } + + fn get_tip_node(&self) -> Result, Self::Error> { + let lock = self.db.read().unwrap(); + let found = lock + .nodes + .rows() + .fold(None, |val: Option<&DbNode>, row| match val { + Some(v) if v.height < row.height => Some(row), + Some(v) => Some(v), + None => Some(row), + }) + .cloned(); + + Ok(found) + } } diff --git a/dan_layer/core/src/storage/mocks/state_db.rs b/dan_layer/core/src/storage/mocks/state_db.rs index d4e272e4cf..2ca153cec0 100644 --- a/dan_layer/core/src/storage/mocks/state_db.rs +++ b/dan_layer/core/src/storage/mocks/state_db.rs @@ -23,7 +23,7 @@ use patricia_tree::PatriciaMap; use crate::storage::{ - state::{DbKeyValue, StateDbBackendAdapter}, + state::{DbKeyValue, DbStateOpLogEntry, StateDbBackendAdapter}, StorageError, }; @@ -83,4 +83,24 @@ impl StateDbBackendAdapter for MockStateDbBackupAdapter { ) -> Result, Self::Error> { todo!() } + + fn get_state_op_logs_by_height( + &self, + _height: u64, + _tx: &Self::BackendTransaction, + ) -> Result, Self::Error> { + todo!() + } + + fn add_state_oplog_entry( + &self, + _entry: DbStateOpLogEntry, + _tx: &Self::BackendTransaction, + ) -> Result<(), Self::Error> { + todo!() + } + + fn clear_all_state(&self, _tx: &Self::BackendTransaction) -> Result<(), Self::Error> { + todo!() + } } diff --git a/dan_layer/core/src/storage/state/db_key_value.rs b/dan_layer/core/src/storage/state/db_key_value.rs index 9193353544..496b53554b 100644 --- a/dan_layer/core/src/storage/state/db_key_value.rs +++ b/dan_layer/core/src/storage/state/db_key_value.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DbKeyValue { pub schema: String, pub key: Vec, diff --git a/dan_layer/core/src/storage/state/mod.rs b/dan_layer/core/src/storage/state/mod.rs index 3bbfad5d62..00eaeb630a 100644 --- a/dan_layer/core/src/storage/state/mod.rs +++ b/dan_layer/core/src/storage/state/mod.rs @@ -20,10 +20,16 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod state_db_unit_of_work; -pub use state_db_unit_of_work::{StateDbUnitOfWork, StateDbUnitOfWorkImpl}; +pub use state_db_unit_of_work::{StateDbUnitOfWork, StateDbUnitOfWorkImpl, StateDbUnitOfWorkReader, UnitOfWorkContext}; + mod db_key_value; pub use db_key_value::DbKeyValue; + mod state_db; pub use state_db::StateDb; + mod state_db_backend_adapter; pub use state_db_backend_adapter::StateDbBackendAdapter; + +mod state_op_log; +pub use state_op_log::{DbStateOpLogEntry, DbStateOperation}; diff --git a/dan_layer/core/src/storage/state/state_db.rs b/dan_layer/core/src/storage/state/state_db.rs index 4efbce10b1..a6452eff81 100644 --- a/dan_layer/core/src/storage/state/state_db.rs +++ b/dan_layer/core/src/storage/state/state_db.rs @@ -20,9 +20,12 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::storage::state::{state_db_unit_of_work::StateDbUnitOfWorkImpl, StateDbBackendAdapter}; +use crate::storage::state::{ + state_db_unit_of_work::{StateDbUnitOfWorkImpl, StateDbUnitOfWorkReader, UnitOfWorkContext}, + StateDbBackendAdapter, +}; -pub struct StateDb { +pub struct StateDb { backend_adapter: TStateDbBackendAdapter, } @@ -31,14 +34,13 @@ impl StateDb StateDbUnitOfWorkImpl { - StateDbUnitOfWorkImpl::new(self.backend_adapter.clone()) + pub fn new_unit_of_work(&self, height: u64) -> StateDbUnitOfWorkImpl { + StateDbUnitOfWorkImpl::new(UnitOfWorkContext::new(height), self.backend_adapter.clone()) + } - // let mut unit_of_work = self.current_unit_of_work_mut(); - // if unit_of_work.is_none() { - // self.unit_of_work = Some(StateDbUnitOfWork {}); - // unit_of_work = self.unit_of_work - // }; - // unit_of_work.as_mut().unwrap() + pub fn reader(&self) -> impl StateDbUnitOfWorkReader { + // TODO: A reader doesnt need the current context, should perhaps make a read-only implementation that the + // writable implementation also uses + StateDbUnitOfWorkImpl::new(UnitOfWorkContext::new(0), self.backend_adapter.clone()) } } diff --git a/dan_layer/core/src/storage/state/state_db_backend_adapter.rs b/dan_layer/core/src/storage/state/state_db_backend_adapter.rs index 02a41406a7..df8b0080aa 100644 --- a/dan_layer/core/src/storage/state/state_db_backend_adapter.rs +++ b/dan_layer/core/src/storage/state/state_db_backend_adapter.rs @@ -22,7 +22,10 @@ use patricia_tree::PatriciaMap; -use crate::storage::{state::db_key_value::DbKeyValue, StorageError}; +use crate::storage::{ + state::{db_key_value::DbKeyValue, DbStateOpLogEntry}, + StorageError, +}; pub trait StateDbBackendAdapter: Send + Sync + Clone { type BackendTransaction; @@ -52,4 +55,12 @@ pub trait StateDbBackendAdapter: Send + Sync + Clone { schema: &str, tx: &Self::BackendTransaction, ) -> Result, Self::Error>; + fn get_state_op_logs_by_height( + &self, + height: u64, + tx: &Self::BackendTransaction, + ) -> Result, Self::Error>; + fn add_state_oplog_entry(&self, entry: DbStateOpLogEntry, tx: &Self::BackendTransaction) + -> Result<(), Self::Error>; + fn clear_all_state(&self, tx: &Self::BackendTransaction) -> Result<(), Self::Error>; } diff --git a/dan_layer/core/src/storage/state/state_db_unit_of_work.rs b/dan_layer/core/src/storage/state/state_db_unit_of_work.rs index 4c715951d9..b973386be6 100644 --- a/dan_layer/core/src/storage/state/state_db_unit_of_work.rs +++ b/dan_layer/core/src/storage/state/state_db_unit_of_work.rs @@ -20,40 +20,67 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::sync::{Arc, RwLock, RwLockReadGuard}; +use std::{ + convert::TryInto, + ops::Deref, + sync::{Arc, RwLock, RwLockReadGuard}, +}; use digest::Digest; +use log::*; use tari_common_types::types::HashDigest; use tari_crypto::common::Blake256; use tari_mmr::{MemBackendVec, MerkleMountainRange}; +use tari_utilities::hex::Hex; use crate::{ - models::StateRoot, + models::{KeyValue, SchemaState, StateOpLogEntry, StateRoot}, storage::{ - state::{db_key_value::DbKeyValue, StateDbBackendAdapter}, + state::{db_key_value::DbKeyValue, DbStateOpLogEntry, StateDbBackendAdapter}, StorageError, UnitOfWorkTracker, }, }; -pub trait StateDbUnitOfWork: Clone + Send + Sync { +const LOG_TARGET: &str = "tari::dan::state_db"; + +pub trait StateDbUnitOfWork: StateDbUnitOfWorkReader { fn set_value(&mut self, schema: String, key: Vec, value: Vec) -> Result<(), StorageError>; + fn set_u64(&mut self, schema: &str, key: &[u8], value: u64) -> Result<(), StorageError>; + fn commit(&mut self) -> Result<(), StorageError>; + fn clear_all_state(&self) -> Result<(), StorageError>; +} + +pub trait StateDbUnitOfWorkReader: Clone + Send + Sync { fn get_value(&mut self, schema: &str, key: &[u8]) -> Result>, StorageError>; fn get_u64(&mut self, schema: &str, key: &[u8]) -> Result, StorageError>; - fn set_u64(&mut self, schema: &str, key: &[u8], value: u64) -> Result<(), StorageError>; fn find_keys_by_value(&self, schema: &str, value: &[u8]) -> Result>, StorageError>; - fn commit(&mut self) -> Result<(), StorageError>; fn calculate_root(&self) -> Result; + fn get_all_state(&self) -> Result, StorageError>; + fn get_op_logs_for_height(&self, height: u64) -> Result, StorageError>; +} + +#[derive(Debug, Clone)] +pub struct UnitOfWorkContext { + pub height: u64, +} + +impl UnitOfWorkContext { + pub fn new(height: u64) -> Self { + Self { height } + } } pub struct StateDbUnitOfWorkImpl { inner: Arc>>, + context: UnitOfWorkContext, } impl StateDbUnitOfWorkImpl { - pub fn new(backend_adapter: TBackendAdapter) -> Self { + pub fn new(context: UnitOfWorkContext, backend_adapter: TBackendAdapter) -> Self { Self { inner: Arc::new(RwLock::new(StateDbUnitOfWorkInner::new(backend_adapter))), + context, } } } @@ -62,6 +89,7 @@ impl Clone for StateDbUnitOfWorkImpl Self { Self { inner: self.inner.clone(), + context: self.context.clone(), } } } @@ -76,6 +104,69 @@ impl StateDbUnitOfWork for StateDbUnitOf Ok(()) } + fn set_u64(&mut self, schema: &str, key: &[u8], value: u64) -> Result<(), StorageError> { + self.set_value(schema.to_string(), Vec::from(key), Vec::from(value.to_le_bytes())) + } + + fn commit(&mut self) -> Result<(), StorageError> { + let mut inner = self.inner.write().unwrap(); + let tx = inner + .backend_adapter + .create_transaction() + .map_err(TBackendAdapter::Error::into)?; + // let mut current_tree = inner + // .backend_adapter + // .get_current_state_tree(&tx) + // .map_err(TBackendAdapter::Error::into)?; + debug!(target: LOG_TARGET, "Committing {} state update(s)", inner.updates.len()); + for item in &inner.updates { + let i = item.get(); + inner + .backend_adapter + .update_key_value(&i.schema, &i.key, &i.value, &tx) + .map_err(TBackendAdapter::Error::into)?; + + inner + .backend_adapter + .add_state_oplog_entry( + DbStateOpLogEntry::set_operation(self.context.height, i.deref().clone()), + &tx, + ) + .map_err(TBackendAdapter::Error::into)?; + // let key = format!("{}.{}", &i.schema, bs58::encode(&i.key).into_string()); + // current_tree.insert(key, i.value.clone()); + } + + // inner + // .backend_adapter + // .set_current_state_tree(current_tree, &tx) + // .map_err(TBackendAdapter::Error::into)?; + + inner + .backend_adapter + .commit(&tx) + .map_err(TBackendAdapter::Error::into)?; + inner.updates = vec![]; + + Ok(()) + } + + /// Clears the state db immediately (before commit) - this will not be needed in future when build up the state from + /// instructions/op logs + fn clear_all_state(&self) -> Result<(), StorageError> { + let inner = self.inner.write().unwrap(); + let tx = inner + .backend_adapter + .create_transaction() + .map_err(TBackendAdapter::Error::into)?; + inner + .backend_adapter + .clear_all_state(&tx) + .map_err(TBackendAdapter::Error::into) + } +} + +impl StateDbUnitOfWorkReader for StateDbUnitOfWorkImpl { fn get_value(&mut self, schema: &str, key: &[u8]) -> Result>, StorageError> { let mut inner = self.inner.write().unwrap(); for v in &inner.updates { @@ -110,17 +201,12 @@ impl StateDbUnitOfWork for StateDbUnitOf Some(data) => { let mut data2: [u8; 8] = [0; 8]; data2.copy_from_slice(&data); - Ok(Some(u64::from_le_bytes(data2))) }, None => Ok(None), } } - fn set_u64(&mut self, schema: &str, key: &[u8], value: u64) -> Result<(), StorageError> { - self.set_value(schema.to_string(), Vec::from(key), Vec::from(value.to_le_bytes())) - } - fn find_keys_by_value(&self, schema: &str, value: &[u8]) -> Result>, StorageError> { let inner = self.inner.read().unwrap(); inner @@ -129,40 +215,6 @@ impl StateDbUnitOfWork for StateDbUnitOf .map_err(TBackendAdapter::Error::into) } - fn commit(&mut self) -> Result<(), StorageError> { - let mut inner = self.inner.write().unwrap(); - let tx = inner - .backend_adapter - .create_transaction() - .map_err(TBackendAdapter::Error::into)?; - // let mut current_tree = inner - // .backend_adapter - // .get_current_state_tree(&tx) - // .map_err(TBackendAdapter::Error::into)?; - for item in &inner.updates { - let i = item.get(); - inner - .backend_adapter - .update_key_value(&i.schema, &i.key, &i.value, &tx) - .map_err(TBackendAdapter::Error::into)?; - // let key = format!("{}.{}", &i.schema, bs58::encode(&i.key).into_string()); - // current_tree.insert(key, i.value.clone()); - } - - // inner - // .backend_adapter - // .set_current_state_tree(current_tree, &tx) - // .map_err(TBackendAdapter::Error::into)?; - - inner - .backend_adapter - .commit(&tx) - .map_err(TBackendAdapter::Error::into)?; - inner.updates = vec![]; - - Ok(()) - } - fn calculate_root(&self) -> Result { let inner = self.inner.read().unwrap(); let tx = inner @@ -173,18 +225,30 @@ impl StateDbUnitOfWork for StateDbUnitOf // omg it's an MMR of MMRs let mut top_level_mmr = MerkleMountainRange::::new(MemBackendVec::new()); - - for schema in inner + let schemas = inner .backend_adapter .get_all_schemas(&tx) - .map_err(TBackendAdapter::Error::into)? - { + .map_err(TBackendAdapter::Error::into)?; + debug!( + target: LOG_TARGET, + "calculate_root: {} key value schemas loaded", + schemas.len() + ); + + for schema in schemas { let mut mmr = MerkleMountainRange::::new(MemBackendVec::new()); for key_value in inner .backend_adapter .get_all_values_for_schema(&schema, &tx) .map_err(TBackendAdapter::Error::into)? { + debug!( + target: LOG_TARGET, + "schema = {}, key = {}, value = {}", + schema, + key_value.key.to_hex(), + key_value.value.to_hex() + ); if let Some(updated_value) = find_update(&inner, &schema, &key_value.key) { let hasher = HashDigest::new(); mmr.push(hasher.chain(&key_value.key).chain(updated_value).finalize().to_vec())?; @@ -196,7 +260,59 @@ impl StateDbUnitOfWork for StateDbUnitOf let hasher = HashDigest::new(); top_level_mmr.push(hasher.chain(schema).chain(mmr.get_merkle_root()?).finalize().to_vec())?; } - Ok(StateRoot::new(top_level_mmr.get_merkle_root()?)) + Ok(StateRoot::new( + top_level_mmr + .get_merkle_root()? + .try_into() + .expect("MMR output incorrect size"), + )) + } + + fn get_all_state(&self) -> Result, StorageError> { + let inner = self.inner.read().unwrap(); + let tx = inner + .backend_adapter + .create_transaction() + .map_err(TBackendAdapter::Error::into)?; + + let schemas = inner + .backend_adapter + .get_all_schemas(&tx) + .map_err(TBackendAdapter::Error::into)?; + let mut schema_state = Vec::with_capacity(schemas.len()); + for schema in schemas { + let key_values = inner + .backend_adapter + .get_all_values_for_schema(&schema, &tx) + .map_err(TBackendAdapter::Error::into)?; + + let key_values = key_values + .into_iter() + .map(|kv| { + let value = find_update(&inner, &schema, &kv.key).unwrap_or(kv.value); + KeyValue { key: kv.key, value } + }) + .collect(); + + schema_state.push(SchemaState::new(schema, key_values)); + } + Ok(schema_state) + } + + fn get_op_logs_for_height(&self, height: u64) -> Result, StorageError> { + let inner = self.inner.read().unwrap(); + let tx = inner + .backend_adapter + .create_transaction() + .map_err(TBackendAdapter::Error::into)?; + + let op_logs = inner + .backend_adapter + .get_state_op_logs_by_height(height, &tx) + .map_err(TBackendAdapter::Error::into)?; + + let op_logs = op_logs.into_iter().map(Into::into).collect(); + Ok(op_logs) } } diff --git a/dan_layer/core/src/storage/state/state_op_log.rs b/dan_layer/core/src/storage/state/state_op_log.rs new file mode 100644 index 0000000000..53a5431d7c --- /dev/null +++ b/dan_layer/core/src/storage/state/state_op_log.rs @@ -0,0 +1,77 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::str::FromStr; + +use crate::{models::TreeNodeHash, storage::state::DbKeyValue}; + +#[derive(Debug)] +pub struct DbStateOpLogEntry { + pub height: u64, + pub merkle_root: Option, + pub operation: DbStateOperation, + pub schema: String, + pub key: Vec, + pub value: Option>, +} + +impl DbStateOpLogEntry { + pub fn set_operation(height: u64, key_value: DbKeyValue) -> Self { + Self { + height, + merkle_root: None, + operation: DbStateOperation::Set, + schema: key_value.schema, + key: key_value.key, + value: Some(key_value.value), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum DbStateOperation { + Set, + Delete, +} + +impl DbStateOperation { + pub fn as_op_str(&self) -> &str { + use DbStateOperation::*; + match self { + Set => "S", + Delete => "D", + } + } +} + +impl FromStr for DbStateOperation { + type Err = (); + + fn from_str(s: &str) -> Result { + use DbStateOperation::*; + match s { + "S" => Ok(Set), + "D" => Ok(Delete), + _ => Err(()), + } + } +} diff --git a/dan_layer/core/src/templates/tip002_template.rs b/dan_layer/core/src/templates/tip002_template.rs index b21d535a21..abe073b841 100644 --- a/dan_layer/core/src/templates/tip002_template.rs +++ b/dan_layer/core/src/templates/tip002_template.rs @@ -25,7 +25,11 @@ use tari_core::transactions::transaction_components::TemplateParameter; use tari_crypto::tari_utilities::{hex::Hex, ByteArray}; use tari_dan_common_types::proto::tips::tip002; -use crate::{models::AssetDefinition, storage::state::StateDbUnitOfWork, DigitalAssetError}; +use crate::{ + models::AssetDefinition, + storage::state::{StateDbUnitOfWork, StateDbUnitOfWorkReader}, + DigitalAssetError, +}; pub fn init( template_parameter: &TemplateParameter, @@ -47,7 +51,7 @@ pub fn init( Ok(()) } -pub fn invoke_read_method( +pub fn invoke_read_method( method: String, args: &[u8], state_db: &mut TUnitOfWork, @@ -69,7 +73,7 @@ pub fn invoke_method( } } -fn balance_of( +fn balance_of( args: &[u8], state_db: &mut TUnitOfWork, ) -> Result>, DigitalAssetError> { diff --git a/dan_layer/core/src/templates/tip004_template.rs b/dan_layer/core/src/templates/tip004_template.rs index 48175ff461..f59473b0d2 100644 --- a/dan_layer/core/src/templates/tip004_template.rs +++ b/dan_layer/core/src/templates/tip004_template.rs @@ -26,7 +26,10 @@ use prost::Message; use tari_crypto::{common::Blake256, tari_utilities::hex::Hex}; use tari_dan_common_types::proto::tips::tip004; -use crate::{storage::state::StateDbUnitOfWork, DigitalAssetError}; +use crate::{ + storage::state::{StateDbUnitOfWork, StateDbUnitOfWorkReader}, + DigitalAssetError, +}; const LOG_TARGET: &str = "tari::dan_layer::core::templates::tip004_template"; @@ -41,7 +44,7 @@ pub fn invoke_method( } } -pub fn invoke_read_method( +pub fn invoke_read_method( method: String, args: &[u8], state_db: &mut TUnitOfWork, @@ -91,7 +94,7 @@ fn hash_of(s: &str) -> Vec { Blake256::new().chain(s).finalize().to_vec() } -fn balance_of( +fn balance_of( args: &[u8], state_db: &mut TUnitOfWork, ) -> Result>, DigitalAssetError> { @@ -111,7 +114,7 @@ fn balance_of( Ok(Some(response_bytes)) } -fn token_of_owner_by_index( +fn token_of_owner_by_index( args: &[u8], state_db: &mut TUnitOfWork, ) -> Result>, DigitalAssetError> { diff --git a/dan_layer/core/src/templates/tip721_template.rs b/dan_layer/core/src/templates/tip721_template.rs index d7a7f6bca4..9fd13a216f 100644 --- a/dan_layer/core/src/templates/tip721_template.rs +++ b/dan_layer/core/src/templates/tip721_template.rs @@ -25,7 +25,10 @@ use prost::Message; use tari_crypto::tari_utilities::{hex::Hex, ByteArray}; use tari_dan_common_types::proto::tips::tip721; -use crate::{storage::state::StateDbUnitOfWork, DigitalAssetError}; +use crate::{ + storage::state::{StateDbUnitOfWork, StateDbUnitOfWorkReader}, + DigitalAssetError, +}; const LOG_TARGET: &str = "tari::dan_layer::core::templates::tip721_template"; @@ -40,7 +43,7 @@ pub fn invoke_method( } } -pub fn invoke_read_method( +pub fn invoke_read_method( method: String, args: &[u8], state_db: &mut TUnitOfWork, @@ -61,7 +64,7 @@ pub fn invoke_read_method( } } -fn owner_of( +fn owner_of( token_id: Vec, state_db: &mut TUnitOfWork, ) -> Result, DigitalAssetError> { diff --git a/dan_layer/core/src/workers/consensus_worker.rs b/dan_layer/core/src/workers/consensus_worker.rs index 3fb0278d5f..f10939c49d 100644 --- a/dan_layer/core/src/workers/consensus_worker.rs +++ b/dan_layer/core/src/workers/consensus_worker.rs @@ -21,6 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use log::*; +use tari_common_types::types::PublicKey; use tari_shutdown::ShutdownSignal; use tokio::time::Duration; @@ -30,7 +31,7 @@ use crate::{ services::{CheckpointManager, CommitteeManager, EventsPublisher, PayloadProvider, ServiceSpecification}, storage::{ chain::ChainDbUnitOfWork, - state::{StateDbUnitOfWork, StateDbUnitOfWorkImpl}, + state::{StateDbUnitOfWork, StateDbUnitOfWorkImpl, StateDbUnitOfWorkReader}, DbFactory, }, workers::{states, states::ConsensusWorkerStateEvent}, @@ -45,7 +46,7 @@ pub struct ConsensusWorker { current_view_id: ViewId, committee_manager: TSpecification::CommitteeManager, timeout: Duration, - node_id: TSpecification::Addr, + node_address: TSpecification::Addr, payload_provider: TSpecification::PayloadProvider, events_publisher: TSpecification::EventsPublisher, signing_service: TSpecification::SigningService, @@ -56,9 +57,10 @@ pub struct ConsensusWorker { chain_storage_service: TSpecification::ChainStorageService, state_db_unit_of_work: Option>, checkpoint_manager: TSpecification::CheckpointManager, + validator_node_client_factory: TSpecification::ValidatorNodeClientFactory, } -impl ConsensusWorker { +impl> ConsensusWorker { pub fn new( inbound_connections: TSpecification::InboundConnectionService, outbound_service: TSpecification::OutboundService, @@ -74,6 +76,7 @@ impl ConsensusWorker { db_factory: TSpecification::DbFactory, chain_storage_service: TSpecification::ChainStorageService, checkpoint_manager: TSpecification::CheckpointManager, + validator_node_client_factory: TSpecification::ValidatorNodeClientFactory, ) -> Self { Self { inbound_connections, @@ -82,7 +85,7 @@ impl ConsensusWorker { timeout, outbound_service, committee_manager, - node_id, + node_address: node_id, payload_provider, events_publisher, signing_service, @@ -93,6 +96,7 @@ impl ConsensusWorker { chain_storage_service, state_db_unit_of_work: None, checkpoint_manager, + validator_node_client_factory, } } @@ -103,7 +107,7 @@ impl ConsensusWorker { .committee_manager .current_committee()? .leader_for_view(self.current_view_id) == - &self.node_id, + &self.node_address, }) } @@ -128,13 +132,11 @@ impl ConsensusWorker { ); break; } - let trns = self.transition(next_event)?; - debug!(target: LOG_TARGET, "Transitioning from {:?} to {:?}", trns.0, trns.1); + let (from, to) = self.transition(next_event)?; + debug!(target: LOG_TARGET, "Transitioning from {:?} to {:?}", from, to); - self.events_publisher.publish(ConsensusWorkerDomainEvent::StateChanged { - old: trns.0, - new: trns.1, - }); + self.events_publisher + .publish(ConsensusWorkerDomainEvent::StateChanged { from, to }); } Ok(()) @@ -156,7 +158,18 @@ impl ConsensusWorker { &self.payload_provider, &self.payload_processor, &self.chain_storage_service, - &self.node_id, + &self.node_address, + ) + .await + }, + Synchronizing => { + states::Synchronizing::new() + .next_event( + &mut self.base_node_client, + &self.asset_definition, + &self.db_factory, + &self.validator_node_client_factory, + &self.node_address, ) .await }, @@ -169,9 +182,9 @@ impl ConsensusWorker { .db_factory .get_state_db(&self.asset_definition.public_key)? .ok_or(DigitalAssetError::MissingDatabase)? - .new_unit_of_work(); + .new_unit_of_work(self.current_view_id.as_u64()); - let mut p = states::Prepare::new(self.node_id.clone(), self.asset_definition.public_key.clone()); + let mut p = states::Prepare::new(self.node_address.clone(), self.asset_definition.public_key.clone()); let res = p .next_event( &self.get_current_view()?, @@ -199,7 +212,7 @@ impl ConsensusWorker { .get_or_create_chain_db(&self.asset_definition.public_key)?; let mut unit_of_work = db.new_unit_of_work(); let mut state = states::PreCommitState::new( - self.node_id.clone(), + self.node_address.clone(), self.committee_manager.current_committee()?.clone(), self.asset_definition.public_key.clone(), ); @@ -223,7 +236,7 @@ impl ConsensusWorker { .get_or_create_chain_db(&self.asset_definition.public_key)?; let mut unit_of_work = db.new_unit_of_work(); let mut state = states::CommitState::new( - self.node_id.clone(), + self.node_address.clone(), self.asset_definition.public_key.clone(), self.committee_manager.current_committee()?.clone(), ); @@ -248,7 +261,7 @@ impl ConsensusWorker { .get_or_create_chain_db(&self.asset_definition.public_key)?; let mut unit_of_work = db.new_unit_of_work(); let mut state = states::DecideState::new( - self.node_id.clone(), + self.node_address.clone(), self.asset_definition.public_key.clone(), self.committee_manager.current_committee()?.clone(), ); @@ -295,7 +308,7 @@ impl ConsensusWorker { &self.db_factory, &mut self.outbound_service, self.committee_manager.current_committee()?, - self.node_id.clone(), + self.node_address.clone(), &self.asset_definition, shutdown, ) @@ -317,7 +330,8 @@ impl ConsensusWorker { use ConsensusWorkerStateEvent::*; let from = self.state; self.state = match (&self.state, event) { - (Starting, Initialized) => NextView, + (Starting, Initialized) => Synchronizing, + (Synchronizing, Synchronized) => NextView, (_, NotPartOfCommittee) => Idle, (Idle, TimedOut) => Starting, (_, TimedOut) => { @@ -332,7 +346,7 @@ impl ConsensusWorker { (PreCommit, PreCommitted) => Commit, (Commit, Committed) => Decide, (Decide, Decided) => NextView, - (Starting, BaseLayerCheckpointNotFound) => { + (_, BaseLayerCheckpointNotFound | BaseLayerAssetRegistrationNotFound) => { unimplemented!("Base layer checkpoint not found!") }, (s, e) => { @@ -454,7 +468,7 @@ mod test { fn assert_state_change(events: &[ConsensusWorkerDomainEvent], states: Vec) { dbg!(events); let mapped_events = events.iter().map(|e| match e { - ConsensusWorkerDomainEvent::StateChanged { old: _, new } => Some(new), + ConsensusWorkerDomainEvent::StateChanged { from: _, to: new } => Some(new), }); for (state, event) in states.iter().zip(mapped_events) { assert_eq!(state, event.unwrap()) diff --git a/dan_layer/core/src/workers/mod.rs b/dan_layer/core/src/workers/mod.rs index 9ad4bd59b5..e1afe1c1a6 100644 --- a/dan_layer/core/src/workers/mod.rs +++ b/dan_layer/core/src/workers/mod.rs @@ -24,3 +24,6 @@ mod consensus_worker; pub mod states; pub use consensus_worker::ConsensusWorker; + +mod state_sync; +pub use state_sync::StateSyncError; diff --git a/dan_layer/core/src/workers/state_sync/error.rs b/dan_layer/core/src/workers/state_sync/error.rs new file mode 100644 index 0000000000..6a68292dba --- /dev/null +++ b/dan_layer/core/src/workers/state_sync/error.rs @@ -0,0 +1,39 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{services::ValidatorNodeClientError, storage::StorageError}; + +#[derive(Debug, thiserror::Error)] +pub enum StateSyncError { + #[error( + "This validator node is the only committee member but its state is not synchronized. Unable to recover state." + )] + NoOtherCommitteeMembersToSync, + #[error("Storage error: {0}")] + StorageError(#[from] StorageError), + #[error("Invalid state Merkle root")] + InvalidStateMerkleRoot, + #[error("Remote peer does not have tip node")] + RemotePeerDoesNotHaveTipNode, + #[error("Validator node client call failed: {0}")] + ValidatorNodeClientError(#[from] ValidatorNodeClientError), +} diff --git a/dan_layer/core/src/workers/state_sync/mod.rs b/dan_layer/core/src/workers/state_sync/mod.rs new file mode 100644 index 0000000000..9577cc79af --- /dev/null +++ b/dan_layer/core/src/workers/state_sync/mod.rs @@ -0,0 +1,141 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +pub use error::StateSyncError; +use log::*; +use rand::{rngs::OsRng, seq::SliceRandom}; +use tari_common_types::types::PublicKey; +use tari_utilities::hex::Hex; + +use crate::{ + models::CheckpointOutput, + services::{ValidatorNodeClientFactory, ValidatorNodeRpcClient}, + storage::{ + state::{StateDb, StateDbBackendAdapter, StateDbUnitOfWork, StateDbUnitOfWorkReader}, + StorageError, + }, +}; + +const LOG_TARGET: &str = "tari::dan::workers::state_sync"; + +pub struct StateSynchronizer<'a, TStateDbBackendAdapter, TValidatorNodeClientFactory: ValidatorNodeClientFactory> { + last_checkpoint: &'a CheckpointOutput, + state_db: &'a mut StateDb, + validator_node_client_factory: &'a TValidatorNodeClientFactory, + our_address: &'a TValidatorNodeClientFactory::Addr, +} + +impl<'a, TStateDbBackendAdapter, TValidatorNodeClientFactory> + StateSynchronizer<'a, TStateDbBackendAdapter, TValidatorNodeClientFactory> +where + TStateDbBackendAdapter: StateDbBackendAdapter, + TValidatorNodeClientFactory: ValidatorNodeClientFactory, +{ + pub fn new( + last_checkpoint: &'a CheckpointOutput, + state_db: &'a mut StateDb, + validator_node_client_factory: &'a TValidatorNodeClientFactory, + our_address: &'a TValidatorNodeClientFactory::Addr, + ) -> Self { + Self { + last_checkpoint, + state_db, + validator_node_client_factory, + our_address, + } + } + + pub async fn sync(&self) -> Result<(), StateSyncError> { + let mut committee = self + .last_checkpoint + .committee + .iter() + .filter(|address| *self.our_address != **address) + .collect::>(); + + if committee.is_empty() { + return Err(StateSyncError::NoOtherCommitteeMembersToSync); + } + + committee.shuffle(&mut OsRng); + + for member in committee { + match self.try_sync_from(member).await { + Ok(_) => { + info!(target: LOG_TARGET, "Sync complete from committee member {}", member); + break; + }, + Err(err) => { + error!(target: LOG_TARGET, "Error syncing from {}: {}", member, err); + continue; + }, + } + } + + Ok(()) + } + + async fn try_sync_from(&self, member: &TValidatorNodeClientFactory::Addr) -> Result<(), StateSyncError> { + info!( + target: LOG_TARGET, + "Attempting to sync asset '{}' from peer '{}'", self.last_checkpoint.parent_public_key, member + ); + let mut client = self.validator_node_client_factory.create_client(member); + let tip_node = client + .get_tip_node(&self.last_checkpoint.parent_public_key) + .await? + .ok_or(StateSyncError::RemotePeerDoesNotHaveTipNode)?; + + // TODO: should rather download the op logs for a checkpoint and reply over initial/current state + let state_schemas = client + .get_sidechain_state(&self.last_checkpoint.parent_public_key) + .await?; + + let mut uow = self.state_db.new_unit_of_work(tip_node.height() as u64); + + for schema in state_schemas { + let name = schema.name; + for item in schema.items { + debug!( + target: LOG_TARGET, + "Adding schema={}, key={}, value={}", + name, + item.key.to_hex(), + item.value.to_hex() + ); + uow.set_value(name.clone(), item.key, item.value)?; + } + } + // TODO: Check merkle root before commit + + uow.clear_all_state().map_err(StorageError::from)?; + uow.commit().map_err(StorageError::from)?; + + let merkle_root = uow.calculate_root()?; + if self.last_checkpoint.merkle_root.as_slice() != merkle_root.as_bytes() { + return Err(StateSyncError::InvalidStateMerkleRoot); + } + + Ok(()) + } +} diff --git a/dan_layer/core/src/workers/states/commit_state.rs b/dan_layer/core/src/workers/states/commit_state.rs index 373cc98883..c79796f1c3 100644 --- a/dan_layer/core/src/workers/states/commit_state.rs +++ b/dan_layer/core/src/workers/states/commit_state.rs @@ -97,32 +97,31 @@ where let next_event_result; loop { tokio::select! { - r = inbound_services.wait_for_message(HotStuffMessageType::PreCommit, current_view.view_id()) => { - let (from, message) = r?; - if current_view.is_leader() { - if let Some(result) = self.process_leader_message(current_view, message.clone(), &from, outbound_service - ).await?{ - next_event_result = result; - break; - } + r = inbound_services.wait_for_message(HotStuffMessageType::PreCommit, current_view.view_id()) => { + let (from, message) = r?; + if current_view.is_leader() { + if let Some(result) = self.process_leader_message(current_view, message.clone(), &from, outbound_service + ).await?{ + next_event_result = result; + break; + } - } - }, - r = inbound_services.wait_for_qc(HotStuffMessageType::PreCommit, current_view.view_id()) => { - let (from, message) = r?; - let leader= self.committee.leader_for_view(current_view.view_id).clone(); - if let Some(result) = self.process_replica_message(&message, current_view, &from, &leader, outbound_service, signing_service, &mut unit_of_work).await? { - next_event_result = result; - break; - } - - } - _ = sleep(timeout.saturating_sub(Instant::now() - started)) => { - // TODO: perhaps this should be from the time the state was entered - next_event_result = ConsensusWorkerStateEvent::TimedOut; - break; - } - } + } + }, + r = inbound_services.wait_for_qc(HotStuffMessageType::PreCommit, current_view.view_id()) => { + let (from, message) = r?; + let leader = self.committee.leader_for_view(current_view.view_id).clone(); + if let Some(result) = self.process_replica_message(&message, current_view, &from, &leader, outbound_service, signing_service, &mut unit_of_work).await? { + next_event_result = result; + break; + } + } + _ = sleep(timeout.saturating_sub(Instant::now() - started)) => { + // TODO: perhaps this should be from the time the state was entered + next_event_result = ConsensusWorkerStateEvent::TimedOut; + break; + } + } } Ok(next_event_result) } diff --git a/dan_layer/core/src/workers/states/mod.rs b/dan_layer/core/src/workers/states/mod.rs index f42db59d7e..1f1c2852d4 100644 --- a/dan_layer/core/src/workers/states/mod.rs +++ b/dan_layer/core/src/workers/states/mod.rs @@ -38,6 +38,7 @@ mod next_view; mod pre_commit_state; mod prepare; mod starting; +mod synchronizing; pub use commit_state::CommitState; pub use decide_state::DecideState; @@ -46,11 +47,14 @@ pub use next_view::NextViewState; pub use pre_commit_state::PreCommitState; pub use prepare::Prepare; pub use starting::Starting; +pub use synchronizing::Synchronizing; #[derive(Debug, PartialEq)] pub enum ConsensusWorkerStateEvent { Initialized, + Synchronized, BaseLayerCheckpointNotFound, + BaseLayerAssetRegistrationNotFound, NotPartOfCommittee, Errored { reason: String }, Prepared, diff --git a/dan_layer/core/src/workers/states/pre_commit_state.rs b/dan_layer/core/src/workers/states/pre_commit_state.rs index 60ffa1ad28..d84785a22c 100644 --- a/dan_layer/core/src/workers/states/pre_commit_state.rs +++ b/dan_layer/core/src/workers/states/pre_commit_state.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{collections::HashMap, marker::PhantomData, time::Instant}; +use std::{collections::HashMap, marker::PhantomData}; use log::*; use tari_common_types::types::PublicKey; @@ -91,39 +91,35 @@ where unit_of_work: TUnitOfWork, ) -> Result { self.received_new_view_messages.clear(); - let started = Instant::now(); let mut unit_of_work = unit_of_work; let next_event_result; + let timeout = sleep(timeout); + futures::pin_mut!(timeout); loop { tokio::select! { - r = inbound_services.wait_for_message(HotStuffMessageType::Prepare, current_view.view_id()) => { + r = inbound_services.wait_for_message(HotStuffMessageType::Prepare, current_view.view_id()) => { let (from, message) = r?; - debug!(target: LOG_TARGET, "Received message: {:?} view:{}", message.message_type(), - message.view_number()); + debug!(target: LOG_TARGET, "Received message: {:?} view:{}", message.message_type(), message.view_number()); if current_view.is_leader() { - - if let Some(result) = self.process_leader_message(current_view, message.clone(), &from, outbound_service - ).await?{ - next_event_result = result; - break; - } - - } - }, - r = inbound_services.wait_for_qc(HotStuffMessageType::Prepare, current_view.view_id()) => { - let (from, message) = r?; - let leader= self.committee.leader_for_view(current_view.view_id).clone(); - if let Some(result) = self.process_replica_message(&message, current_view, &from, &leader, outbound_service, signing_service, &mut unit_of_work).await? { - next_event_result = result; - break; - } - - }, - _ = sleep(timeout.saturating_sub(Instant::now() - started)) => { - next_event_result = ConsensusWorkerStateEvent::TimedOut; - break; - } - } + if let Some(result) = self.process_leader_message(current_view, message.clone(), &from, outbound_service).await? { + next_event_result = result; + break; + } + } + }, + r = inbound_services.wait_for_qc(HotStuffMessageType::Prepare, current_view.view_id()) => { + let (from, message) = r?; + let leader = self.committee.leader_for_view(current_view.view_id).clone(); + if let Some(result) = self.process_replica_message(&message, current_view, &from, &leader, outbound_service, signing_service, &mut unit_of_work).await? { + next_event_result = result; + break; + } + }, + _ = &mut timeout => { + next_event_result = ConsensusWorkerStateEvent::TimedOut; + break; + } + } } Ok(next_event_result) } diff --git a/dan_layer/core/src/workers/states/prepare.rs b/dan_layer/core/src/workers/states/prepare.rs index 4fc82dd2e3..f72ba0f401 100644 --- a/dan_layer/core/src/workers/states/prepare.rs +++ b/dan_layer/core/src/workers/states/prepare.rs @@ -154,10 +154,9 @@ where if current_view.is_leader() { if let Some(result) = self.process_leader_message(current_view, message.clone(), &from, committee, payload_provider, payload_processor, outbound_service, db_factory).await?{ - next_event_result = result; + next_event_result = result; break; } - } }, r = inbound_services.wait_for_message(HotStuffMessageType::Prepare, current_view.view_id()) => { @@ -220,13 +219,13 @@ where let temp_state_tx = db_factory .get_or_create_state_db(&self.asset_public_key)? - .new_unit_of_work(); + .new_unit_of_work(current_view.view_id.as_u64()); let proposal = self .create_proposal( *high_qc.node_hash(), payload_provider, payload_processor, - 0, + current_view.view_id.as_u64() as u32, temp_state_tx, ) .await?; diff --git a/dan_layer/core/src/workers/states/starting.rs b/dan_layer/core/src/workers/states/starting.rs index 915df3d8fd..101e4925c0 100644 --- a/dan_layer/core/src/workers/states/starting.rs +++ b/dan_layer/core/src/workers/states/starting.rs @@ -105,6 +105,7 @@ where TBaseNodeClient: BaseNodeClient ); return Ok(ConsensusWorkerStateEvent::NotPartOfCommittee); } + info!( target: LOG_TARGET, "Validator node is a committee member for asset public key '{}'", @@ -118,7 +119,7 @@ where TBaseNodeClient: BaseNodeClient let mut tx = chain_db.new_unit_of_work(); let state_db = db_factory.get_or_create_state_db(&asset_definition.public_key)?; - let mut state_tx = state_db.new_unit_of_work(); + let mut state_tx = state_db.new_unit_of_work(0); info!(target: LOG_TARGET, "Loading initial state"); let initial_state = asset_definition.initial_state(); diff --git a/dan_layer/core/src/workers/states/synchronizing.rs b/dan_layer/core/src/workers/states/synchronizing.rs new file mode 100644 index 0000000000..e08ec84592 --- /dev/null +++ b/dan_layer/core/src/workers/states/synchronizing.rs @@ -0,0 +1,119 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::convert::TryFrom; + +use log::*; +use tari_common_types::types::PublicKey; + +use crate::{ + models::{AssetDefinition, CheckpointOutput}, + services::{BaseNodeClient, ValidatorNodeClientFactory}, + storage::{state::StateDbUnitOfWorkReader, DbFactory}, + workers::{state_sync::StateSynchronizer, states::ConsensusWorkerStateEvent}, + DigitalAssetError, +}; + +const LOG_TARGET: &str = "tari::dan::workers::states::starting"; + +#[derive(Debug, Clone, Default)] +pub struct Synchronizing; + +impl Synchronizing { + pub fn new() -> Self { + Self + } + + #[allow(unreachable_code, unused_variables)] + pub async fn next_event( + &mut self, + base_node_client: &mut TBaseNodeClient, + asset_definition: &AssetDefinition, + db_factory: &TDbFactory, + validator_node_client_factory: &TValidatorNodeClientFactory, + our_address: &TValidatorNodeClientFactory::Addr, + ) -> Result + where + TDbFactory: DbFactory, + TBaseNodeClient: BaseNodeClient, + TValidatorNodeClientFactory: ValidatorNodeClientFactory, + { + // TODO: The collectibles app does not post a valid initial merkle root for the initial asset checkpoint. So + // this is always out-of-sync. + // return Ok(ConsensusWorkerStateEvent::Synchronized); + + let tip = base_node_client.get_tip_info().await?; + let last_checkpoint = base_node_client + .get_current_checkpoint( + tip.height_of_longest_chain - asset_definition.base_layer_confirmation_time, + asset_definition.public_key.clone(), + asset_definition.checkpoint_unique_id.clone(), + ) + .await?; + + let last_checkpoint = match last_checkpoint { + Some(cp) => CheckpointOutput::try_from(cp)?, + None => return Ok(ConsensusWorkerStateEvent::BaseLayerCheckpointNotFound), + }; + + let asset_registration = base_node_client + .get_asset_registration(asset_definition.public_key.clone()) + .await?; + + let mut state_db = db_factory.get_or_create_state_db(&asset_definition.public_key)?; + { + let state_reader = state_db.reader(); + let our_merkle_root = state_reader.calculate_root()?; + if our_merkle_root.as_bytes() == last_checkpoint.merkle_root.as_slice() { + info!(target: LOG_TARGET, "Our state database is up-to-date."); + return Ok(ConsensusWorkerStateEvent::Synchronized); + } + let registration_merkle_root = asset_registration.and_then(|ar| ar.get_checkpoint_merkle_root()); + if registration_merkle_root + .map(|mr| our_merkle_root.as_bytes() == mr.as_slice()) + .unwrap_or(false) + { + info!( + target: LOG_TARGET, + "Our state database is up-to-date (at initial state)." + ); + return Ok(ConsensusWorkerStateEvent::Synchronized); + } + } + + info!( + target: LOG_TARGET, + "Our state database for asset '{}' is out of sync. Attempting to contact a committee member to synchronize", + asset_definition.public_key + ); + + let synchronizer = StateSynchronizer::new( + &last_checkpoint, + &mut state_db, + validator_node_client_factory, + our_address, + ); + synchronizer.sync().await?; + + Ok(ConsensusWorkerStateEvent::Synchronized) + } +} diff --git a/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/down.sql b/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/down.sql new file mode 100644 index 0000000000..291a97c5ce --- /dev/null +++ b/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/up.sql b/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/up.sql new file mode 100644 index 0000000000..e15f19bb7f --- /dev/null +++ b/dan_layer/storage_sqlite/migrations/2022-02-09-105823_create_state_op_log/up.sql @@ -0,0 +1,13 @@ +create table state_op_log +( + id integer primary key autoincrement not null, + height bigint not null, + merkle_root blob(32) null, + operation varchar(30) not null, + schema varchar(255) not null, + key blob not null, + value blob null +); + +create index state_op_log_height_index on state_op_log (height); +create index state_op_log_merkle_root_index on state_op_log (merkle_root); diff --git a/dan_layer/storage_sqlite/src/error.rs b/dan_layer/storage_sqlite/src/error.rs index 4bc6f3d904..827e66d435 100644 --- a/dan_layer/storage_sqlite/src/error.rs +++ b/dan_layer/storage_sqlite/src/error.rs @@ -44,6 +44,8 @@ pub enum SqliteStorageError { DecodeError(#[from] bytecodec::Error), #[error("Encountered malformed hash data")] MalformedHashData, + #[error("Malformed DB data: {0}")] + MalformedDbData(String), #[error(transparent)] ModelError(#[from] ModelError), } diff --git a/dan_layer/storage_sqlite/src/models/mod.rs b/dan_layer/storage_sqlite/src/models/mod.rs index e7372965ac..00613a6de0 100644 --- a/dan_layer/storage_sqlite/src/models/mod.rs +++ b/dan_layer/storage_sqlite/src/models/mod.rs @@ -25,4 +25,5 @@ pub mod locked_qc; pub mod node; pub mod prepare_qc; pub mod state_key; +pub mod state_op_log; pub mod state_tree; diff --git a/dan_layer/storage_sqlite/src/models/node.rs b/dan_layer/storage_sqlite/src/models/node.rs index 9c665ba800..e310ab0d2e 100644 --- a/dan_layer/storage_sqlite/src/models/node.rs +++ b/dan_layer/storage_sqlite/src/models/node.rs @@ -22,7 +22,7 @@ use crate::schema::*; -#[derive(Identifiable, Queryable)] +#[derive(Debug, Clone, Identifiable, Queryable)] pub struct Node { pub id: i32, pub hash: Vec, @@ -31,7 +31,7 @@ pub struct Node { pub is_committed: bool, } -#[derive(Insertable)] +#[derive(Debug, Clone, Insertable)] #[table_name = "nodes"] pub struct NewNode { pub hash: Vec, diff --git a/dan_layer/storage_sqlite/src/models/state_op_log.rs b/dan_layer/storage_sqlite/src/models/state_op_log.rs new file mode 100644 index 0000000000..86b1ab2d40 --- /dev/null +++ b/dan_layer/storage_sqlite/src/models/state_op_log.rs @@ -0,0 +1,86 @@ +use std::convert::TryFrom; + +use tari_dan_core::models::TreeNodeHash; +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that +// the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the +// following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +// DAMAGE. +use tari_dan_core::storage::state::DbStateOpLogEntry; + +use crate::{error::SqliteStorageError, schema::*}; + +#[derive(Debug, Clone, Identifiable, Queryable)] +#[table_name = "state_op_log"] +pub struct StateOpLogEntry { + pub id: i32, + pub height: i64, + pub merkle_root: Option>, + pub operation: String, + pub schema: String, + pub key: Vec, + pub value: Option>, +} + +#[derive(Debug, Clone, Insertable)] +#[table_name = "state_op_log"] +pub struct NewStateOpLogEntry { + pub height: i64, + pub merkle_root: Option>, + pub operation: String, + pub schema: String, + pub key: Vec, + pub value: Option>, +} + +impl From for NewStateOpLogEntry { + fn from(entry: DbStateOpLogEntry) -> Self { + Self { + height: entry.height as i64, + merkle_root: entry.merkle_root.map(|r| r.as_bytes().to_vec()), + operation: entry.operation.as_op_str().to_string(), + schema: entry.schema, + key: entry.key, + value: entry.value, + } + } +} + +impl TryFrom for DbStateOpLogEntry { + type Error = SqliteStorageError; + + fn try_from(entry: StateOpLogEntry) -> Result { + Ok(Self { + height: entry.height as u64, + merkle_root: entry + .merkle_root + .map(TreeNodeHash::try_from) + .transpose() + .map_err(|_| SqliteStorageError::MalformedHashData)?, + operation: entry + .operation + .parse() + .map_err(|_| SqliteStorageError::MalformedDbData("Invalid OpLog operation".to_string()))?, + schema: entry.schema, + key: entry.key, + value: entry.value, + }) + } +} diff --git a/dan_layer/storage_sqlite/src/schema.rs b/dan_layer/storage_sqlite/src/schema.rs index 0b97932414..9cb548dc3c 100644 --- a/dan_layer/storage_sqlite/src/schema.rs +++ b/dan_layer/storage_sqlite/src/schema.rs @@ -47,6 +47,18 @@ table! { } } +table! { + state_op_log (id) { + id -> Integer, + height -> BigInt, + merkle_root -> Nullable, + operation -> Text, + schema -> Text, + key -> Binary, + value -> Nullable, + } +} + table! { state_tree (id) { id -> Integer, @@ -58,4 +70,12 @@ table! { joinable!(instructions -> nodes (node_id)); -allow_tables_to_appear_in_same_query!(instructions, locked_qc, nodes, prepare_qc, state_keys, state_tree,); +allow_tables_to_appear_in_same_query!( + instructions, + locked_qc, + nodes, + prepare_qc, + state_keys, + state_op_log, + state_tree, +); diff --git a/dan_layer/storage_sqlite/src/sqlite_chain_backend_adapter.rs b/dan_layer/storage_sqlite/src/sqlite_chain_backend_adapter.rs index ac57779dd2..83ece5b6da 100644 --- a/dan_layer/storage_sqlite/src/sqlite_chain_backend_adapter.rs +++ b/dan_layer/storage_sqlite/src/sqlite_chain_backend_adapter.rs @@ -52,6 +52,10 @@ impl SqliteChainBackendAdapter { pub fn new(database_url: String) -> SqliteChainBackendAdapter { Self { database_url } } + + pub fn get_connection(&self) -> ConnectionResult { + SqliteConnection::establish(self.database_url.as_str()) + } } impl ChainDbBackendAdapter for SqliteChainBackendAdapter { @@ -61,7 +65,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { type Payload = TariDanPayload; fn is_empty(&self) -> Result { - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; let n: Option = nodes::table .first(&connection) @@ -73,8 +77,26 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { Ok(n.is_none()) } + fn create_transaction(&self) -> Result { + let connection = self.get_connection()?; + connection + .execute("PRAGMA foreign_keys = ON;") + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "set pragma".to_string(), + })?; + connection + .execute("BEGIN EXCLUSIVE TRANSACTION;") + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "begin transaction".to_string(), + })?; + + Ok(SqliteTransaction::new(connection)) + } + fn node_exists(&self, node_hash: &TreeNodeHash) -> Result { - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; use crate::schema::nodes::dsl; let count = dsl::nodes .filter(nodes::parent.eq(node_hash.as_bytes())) @@ -89,22 +111,28 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { Ok(count > 0) } - fn create_transaction(&self) -> Result { - let connection = SqliteConnection::establish(self.database_url.as_str())?; - connection - .execute("PRAGMA foreign_keys = ON;") - .map_err(|source| SqliteStorageError::DieselError { - source, - operation: "set pragma".to_string(), - })?; - connection - .execute("BEGIN EXCLUSIVE TRANSACTION;") + fn get_tip_node(&self) -> Result, Self::Error> { + use crate::schema::nodes::dsl; + + let connection = self.get_connection()?; + let node = dsl::nodes + .order_by(dsl::height.desc()) + .first::(&connection) + .optional() .map_err(|source| SqliteStorageError::DieselError { source, - operation: "begin transaction".to_string(), + operation: "get_tip_node".to_string(), })?; - Ok(SqliteTransaction::new(connection)) + match node { + Some(node) => Ok(Some(DbNode { + hash: node.hash.try_into()?, + parent: node.parent.try_into()?, + height: node.height as u32, + is_committed: node.is_committed, + })), + None => Ok(None), + } } fn insert_node(&self, item: &DbNode, transaction: &Self::BackendTransaction) -> Result<(), Self::Error> { @@ -224,7 +252,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { } fn get_prepare_qc(&self) -> Result, Self::Error> { - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; use crate::schema::prepare_qc::dsl; let qc: Option = dsl::prepare_qc .find(1) @@ -249,7 +277,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { debug!(target: LOG_TARGET, "Committing transaction"); transaction .connection() - .execute("COMMIT TRANSACTION;") + .execute("COMMIT TRANSACTION") .map_err(|source| SqliteStorageError::DieselError { source, operation: "commit::chain".to_string(), @@ -267,7 +295,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { fn find_highest_prepared_qc(&self) -> Result { use crate::schema::locked_qc::dsl; - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; // TODO: this should be a single row let result: Option = prepare_qc::table .order_by(prepare_qc::view_number.desc()) @@ -307,7 +335,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { fn get_locked_qc(&self) -> Result { use crate::schema::locked_qc::dsl; - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; let qc: LockedQc = dsl::locked_qc .find(self.locked_qc_id()) .first(&connection) @@ -325,7 +353,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { fn find_node_by_hash(&self, parent_hash: &TreeNodeHash) -> Result, Self::Error> { use crate::schema::nodes::dsl; - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; let node = dsl::nodes .filter(nodes::parent.eq(parent_hash.as_bytes())) .first::(&connection) @@ -348,7 +376,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { fn find_node_by_parent_hash(&self, node_hash: &TreeNodeHash) -> Result, Self::Error> { use crate::schema::nodes::dsl; - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; let node = dsl::nodes .filter(nodes::parent.eq(node_hash.as_bytes())) .first::(&connection) @@ -402,7 +430,7 @@ impl ChainDbBackendAdapter for SqliteChainBackendAdapter { fn find_all_instructions_by_node(&self, node_id: Self::Id) -> Result, Self::Error> { use crate::schema::{instructions::dsl as instructions_dsl, nodes::dsl as nodes_dsl}; - let connection = SqliteConnection::establish(self.database_url.as_str())?; + let connection = self.get_connection()?; let node = nodes_dsl::nodes .filter(nodes::id.eq(node_id)) .first::(&connection) diff --git a/dan_layer/storage_sqlite/src/sqlite_state_db_backend_adapter.rs b/dan_layer/storage_sqlite/src/sqlite_state_db_backend_adapter.rs index 62a6b4fa02..3b5b9af7b6 100644 --- a/dan_layer/storage_sqlite/src/sqlite_state_db_backend_adapter.rs +++ b/dan_layer/storage_sqlite/src/sqlite_state_db_backend_adapter.rs @@ -20,6 +20,8 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::convert::TryInto; + use bytecodec::{ bincode_codec::{BincodeDecoder, BincodeEncoder}, DecodeExt, @@ -31,12 +33,13 @@ use patricia_tree::{ node::{Node, NodeDecoder, NodeEncoder}, PatriciaMap, }; -use tari_dan_core::storage::state::{DbKeyValue, StateDbBackendAdapter}; +use tari_dan_core::storage::state::{DbKeyValue, DbStateOpLogEntry, StateDbBackendAdapter}; use crate::{ error::SqliteStorageError, models::{ state_key::StateKey, + state_op_log::{NewStateOpLogEntry, StateOpLogEntry}, state_tree::{NewStateTree, StateTree}, }, schema::*, @@ -257,4 +260,57 @@ impl StateDbBackendAdapter for SqliteStateDbBackendAdapter { .map(|(schema, key, value)| DbKeyValue { schema, key, value }) .collect()) } + + fn get_state_op_logs_by_height( + &self, + height: u64, + tx: &Self::BackendTransaction, + ) -> Result, Self::Error> { + use crate::schema::state_op_log::dsl; + let op_logs = dsl::state_op_log + .filter(dsl::height.eq(height as i64)) + .order_by(dsl::key.asc()) + .load::(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "get_all_values_for_schema".to_string(), + })?; + + op_logs.into_iter().map(TryInto::try_into).collect() + } + + fn add_state_oplog_entry( + &self, + entry: DbStateOpLogEntry, + tx: &Self::BackendTransaction, + ) -> Result<(), Self::Error> { + use crate::schema::state_op_log::dsl; + diesel::insert_into(dsl::state_op_log) + .values(NewStateOpLogEntry::from(entry)) + .execute(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "add_state_oplog_entry".to_string(), + })?; + + Ok(()) + } + + fn clear_all_state(&self, tx: &Self::BackendTransaction) -> Result<(), Self::Error> { + diesel::delete(state_keys::dsl::state_keys) + .execute(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "clear_all_state::state_keys".to_string(), + })?; + + diesel::delete(state_op_log::dsl::state_op_log) + .execute(tx.connection()) + .map_err(|source| SqliteStorageError::DieselError { + source, + operation: "clear_all_state::state_op_logs".to_string(), + })?; + + Ok(()) + } } From 90261526683c940c8aebe224d0d666931d4de11e Mon Sep 17 00:00:00 2001 From: ZHANG Cheng Date: Tue, 15 Feb 2022 17:39:54 +0800 Subject: [PATCH 18/28] feat(cli): resize terminal height (#3838) Description --- To resolve #1728 better Motivation and Context --- As a follow-up of #3827. How Has This Been Tested? --- Tested on macOS Monterey (12.1). Works for system Terminal app. Doesn't work for iTerm2. --- applications/tari_base_node/src/cli.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/applications/tari_base_node/src/cli.rs b/applications/tari_base_node/src/cli.rs index da696c867c..cab727d892 100644 --- a/applications/tari_base_node/src/cli.rs +++ b/applications/tari_base_node/src/cli.rs @@ -23,10 +23,7 @@ use std::io::stdout; use chrono::{Datelike, Utc}; -use crossterm::{ - execute, - terminal::{size, SetSize}, -}; +use crossterm::{execute, terminal::SetSize}; use tari_app_utilities::consts; /// returns the top or bottom box line of the specified length @@ -108,11 +105,9 @@ fn multiline_find_display_length(lines: &str) -> usize { /// Try to resize terminal to make sure the width is enough. /// In case of error, just simply print out the error. -fn resize_terminal_to_fit_the_box(width: usize) { - if let Ok((_, rows)) = size() { - if let Err(e) = execute!(stdout(), SetSize(width as u16, rows)) { - println!("Can't resize terminal to fit the box. Error: {}", e) - } +fn resize_terminal_to_fit_the_box(width: usize, height: usize) { + if let Err(e) = execute!(stdout(), SetSize(width as u16, height as u16)) { + println!("Can't resize terminal to fit the box. Error: {}", e) } } @@ -158,8 +153,6 @@ pub fn print_banner(commands: Vec, chunk_size: i32) { let banner = include!("../assets/tari_banner.rs"); let target_line_length = multiline_find_display_length(banner); - resize_terminal_to_fit_the_box(target_line_length); - for line in banner.lines() { println!("{}", line); } @@ -187,8 +180,13 @@ pub fn print_banner(commands: Vec, chunk_size: i32) { println!("{}", box_data("~~~~~~~~".to_string(), target_line_length)); println!("{}", box_separator(target_line_length)); let rows = box_tabular_data_rows(command_data, row_cell_size, target_line_length, 10); + // There are 24 fixed rows besides the possible changed "Commands" rows + // and plus 2 more blank rows for better layout. + let height_to_resize = &rows.len() + 24 + 2; for row in rows { println!("{}", row); } println!("{}", box_line(target_line_length, false)); + + resize_terminal_to_fit_the_box(target_line_length, height_to_resize); } From 5cda980d92fb3c4617836611dcfb59e01c58cec6 Mon Sep 17 00:00:00 2001 From: Mike the Tike Date: Tue, 15 Feb 2022 12:27:10 +0200 Subject: [PATCH 19/28] fix(dan): include state_root in node hash (#3836) Small bug that state_root was missing from node hash --- dan_layer/core/src/models/hot_stuff_tree_node.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dan_layer/core/src/models/hot_stuff_tree_node.rs b/dan_layer/core/src/models/hot_stuff_tree_node.rs index 66458c8a58..19d319ad16 100644 --- a/dan_layer/core/src/models/hot_stuff_tree_node.rs +++ b/dan_layer/core/src/models/hot_stuff_tree_node.rs @@ -73,6 +73,7 @@ impl HotStuffTreeNode { .chain(self.parent.as_bytes()) .chain(self.payload.consensus_hash()) .chain(self.height.to_le_bytes()) + .chain(self.state_root.as_bytes()) .finalize_fixed(); result.into() } From a71645dd8e563ba6e49786847f09d2c1e319be40 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Tue, 15 Feb 2022 12:08:14 +0100 Subject: [PATCH 20/28] chore: change NodeIdentity debug (#3817) Description --- Remove secret_key from debug print for `NodeIdentity`. The display is using the same fields that are used in the `fmt::Display` trait --- comms/src/peer_manager/node_identity.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/comms/src/peer_manager/node_identity.rs b/comms/src/peer_manager/node_identity.rs index 2b39ce7085..3fd7d8aadd 100644 --- a/comms/src/peer_manager/node_identity.rs +++ b/comms/src/peer_manager/node_identity.rs @@ -41,7 +41,7 @@ use crate::{ }; /// The public and private identity of this node on the network -#[derive(Debug, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub struct NodeIdentity { #[serde(serialize_with = "serialize_to_hex")] #[serde(deserialize_with = "deserialize_node_id_from_hex")] @@ -221,3 +221,16 @@ impl fmt::Display for NodeIdentity { Ok(()) } } + +impl fmt::Debug for NodeIdentity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NodeIdentity") + .field("public_key", &self.public_key) + .field("node_id", &self.node_id) + .field("public_address", &self.public_address) + .field("features", &self.features) + .field("secret_key", &"") + .field("identity_signature", &*acquire_read_lock!(self.identity_signature)) + .finish() + } +} From 22416a1c2efd9328f35f11ad89bed3fb845c9f72 Mon Sep 17 00:00:00 2001 From: ZHANG Cheng Date: Wed, 16 Feb 2022 15:59:02 +0800 Subject: [PATCH 21/28] fix: update RFC links and README (#3675) (#3839) Description --- Update in light of #3675. For reason unknown to me, the link to RFC_template.md also has to appear in SUMMARY.md. Otherwise, it will be a 404 as reported (somehow the HTML file is not generated). Motivation and Context --- See above. How Has This Been Tested? --- Manual check. --- RFC/README.md | 10 +++++++++- RFC/src/SUMMARY.md | 1 + RFC/src/about.md | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/RFC/README.md b/RFC/README.md index fc8aa7a21a..9c537651f8 100644 --- a/RFC/README.md +++ b/RFC/README.md @@ -2,4 +2,12 @@ RFC documents for the Tari protocol -The rendered version of these docs can be found at https://rfc.tari.com \ No newline at end of file +These documents are in the form of an mdbook. The rendered version of these docs can be found at https://rfc.tari.com + +## View local rendered version + +- run `cargo install mdbook` + +- in the `/RFC` folder run `mdbook serve` + +- in a browser navigate to [`localhost:3000`](http://localhost:3000) diff --git a/RFC/src/SUMMARY.md b/RFC/src/SUMMARY.md index 683f6adc34..9d384e4e7e 100644 --- a/RFC/src/SUMMARY.md +++ b/RFC/src/SUMMARY.md @@ -48,4 +48,5 @@ - [RFC-0130: Mining](RFC-0130_Mining.md) +- [RFC template](RFC_template.md) - [Glossary](Glossary.md) diff --git a/RFC/src/about.md b/RFC/src/about.md index cc7abe1529..61448748b8 100644 --- a/RFC/src/about.md +++ b/RFC/src/about.md @@ -9,10 +9,10 @@ iterations before reaching this point: not cast in stone. RFCs can, and should, undergo further evaluation and discussion by the community. RFC comments are best made using Github [issue]s. -New RFC's should follow the format given in the [RFC template](./RFC_template.md). +New RFC's should follow the format given in the [RFC template](RFC_template.md). ## Lifecycle -RFCs go through the following lifecycle, which roughly corresponds to the [COSS](https://rfc.unprotocols.org/spec:2/COSS/): +RFCs go through the following lifecycle, which roughly corresponds to the [COSS](https://github.com/unprotocols/rfc/blob/master/2/README.md): | Status | | Description | |:------------|:--------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| From 31410cd05c14751136a93ec543c9822fd8221e18 Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Wed, 16 Feb 2022 15:41:22 +0200 Subject: [PATCH 22/28] feat: add persistence of transaction cancellation reason to wallet db (#3842) Description --- This PR adds the persistence of the reason a transaction was rejected or cancelled to the db. This is done by expanding the existing `cancelled` field on a CompletedTransaction too not just be a boolean but an integer encoded nullable Enum which will encode the reason for the cancellation. By expanding the information contained by this field we no longer need the `valid` field which was used to indicate when a Coinbase Transaction was abandoned and we just make that a cancellation reason. A diesel migration is provided to handle this change is db schema. Some specific areas changed to handle the update: 1. The console wallet was updated to take this change into account when displaying transactions. 2. The transaction validation protocol was updated to handle coinbases using this field rather than the valid field 3. Transaction database methods were updated to take the change into account In the FFI we do the following: 1. Remove the `completed_transaction_is_valid` method for the valid field that is taken out. 2. Added the `completed_transaction_get_cancellation_reason` method that returns whether the cancelled field is null as -1 or else the integer encoding of the TxCancellationReason enum. ```C /// Gets the reason a TariCompletedTransaction is cancelled, if it is indeed cancelled /// /// ## Arguments /// `tx` - The TariCompletedTransaction /// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions /// as an out parameter. /// /// ## Returns /// `c_int` - Returns the reason for cancellation which corresponds to: /// | Value | Interpretation | /// |--- |--- | /// | -1 | Not Cancelled | /// | 0 | Unknown | /// | 1 | UserCancelled | /// | 2 | Timeout | /// | 3 | DoubleSpend | /// | 4 | Orphan | /// | 5 | TimeLocked | /// | 6 | InvalidTransaction | /// | 7 | AbandonedCoinbase | /// # Safety /// None int completed_transaction_get_cancellation_reason(struct TariCompletedTransaction *transaction, int *error_out); ``` How Has This Been Tested? --- Tests are updated to take this change into account. --- applications/tari_app_grpc/proto/wallet.proto | 1 - .../src/grpc/wallet_grpc_server.rs | 8 +- .../src/ui/components/transactions_tab.rs | 39 ++-- .../src/ui/state/app_state.rs | 15 +- .../down.sql | 1 + .../up.sql | 60 +++++ .../src/output_manager_service/service.rs | 19 +- base_layer/wallet/src/schema.rs | 3 +- .../wallet/src/transaction_service/error.rs | 2 + .../wallet/src/transaction_service/handle.rs | 11 +- .../src/transaction_service/protocols/mod.rs | 11 - .../transaction_broadcast_protocol.rs | 60 ++--- .../protocols/transaction_receive_protocol.rs | 5 +- .../protocols/transaction_send_protocol.rs | 5 +- .../transaction_validation_protocol.rs | 17 +- .../wallet/src/transaction_service/service.rs | 7 +- .../transaction_service/storage/database.rs | 36 ++- .../src/transaction_service/storage/models.rs | 78 ++++++- .../transaction_service/storage/sqlite_db.rs | 217 ++++++++++++------ .../tasks/check_faux_transaction_status.rs | 1 - .../transaction_service_tests/service.rs | 60 ++--- .../transaction_service_tests/storage.rs | 16 +- .../transaction_protocols.rs | 34 ++- .../wallet_ffi/src/callback_handler_tests.rs | 13 +- base_layer/wallet_ffi/src/lib.rs | 63 +++-- base_layer/wallet_ffi/wallet.h | 31 ++- integration_tests/features/WalletCli.feature | 14 +- integration_tests/features/WalletFFI.feature | 4 +- .../features/WalletMonitoring.feature | 6 +- .../features/support/wallet_steps.js | 28 +-- .../helpers/ffi/completedTransaction.js | 4 - integration_tests/helpers/ffi/ffiInterface.js | 26 ++- integration_tests/helpers/util.js | 4 +- integration_tests/helpers/walletClient.js | 46 +--- integration_tests/helpers/walletFFIClient.js | 4 + 35 files changed, 582 insertions(+), 367 deletions(-) create mode 100644 base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/down.sql create mode 100644 base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/up.sql diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index 29c9f8b00b..acdd7086b6 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -166,7 +166,6 @@ message TransactionInfo { bytes excess_sig = 9; google.protobuf.Timestamp timestamp = 10; string message = 11; - bool valid = 12; } enum TransactionDirection { diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index b9aff22252..3d4366fb7f 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -539,7 +539,7 @@ impl wallet_server::Wallet for WalletGrpcServer { dest_pk: txn.destination_public_key.to_vec(), status: TransactionStatus::from(txn.status) as i32, amount: txn.amount.into(), - is_cancelled: txn.cancelled, + is_cancelled: txn.cancelled.is_some(), direction: TransactionDirection::from(txn.direction) as i32, fee: txn.fee.into(), timestamp: Some(naive_datetime_to_timestamp(txn.timestamp)), @@ -550,7 +550,6 @@ impl wallet_server::Wallet for WalletGrpcServer { .get_signature() .to_vec(), message: txn.message, - valid: txn.valid, }), }; match sender.send(Ok(response)).await { @@ -931,7 +930,6 @@ fn convert_wallet_transaction_into_transaction_info( excess_sig: Default::default(), timestamp: Some(naive_datetime_to_timestamp(tx.timestamp)), message: tx.message, - valid: true, }, PendingOutbound(tx) => TransactionInfo { tx_id: tx.tx_id.into(), @@ -945,7 +943,6 @@ fn convert_wallet_transaction_into_transaction_info( excess_sig: Default::default(), timestamp: Some(naive_datetime_to_timestamp(tx.timestamp)), message: tx.message, - valid: true, }, Completed(tx) => TransactionInfo { tx_id: tx.tx_id.into(), @@ -953,7 +950,7 @@ fn convert_wallet_transaction_into_transaction_info( dest_pk: tx.destination_public_key.to_vec(), status: TransactionStatus::from(tx.status) as i32, amount: tx.amount.into(), - is_cancelled: tx.cancelled, + is_cancelled: tx.cancelled.is_some(), direction: TransactionDirection::from(tx.direction) as i32, fee: tx.fee.into(), timestamp: Some(naive_datetime_to_timestamp(tx.timestamp)), @@ -963,7 +960,6 @@ fn convert_wallet_transaction_into_transaction_info( .map(|s| s.get_signature().to_vec()) .unwrap_or_default(), message: tx.message, - valid: tx.valid, }, } } diff --git a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs index 323df3f79f..8f59431f48 100644 --- a/applications/tari_console_wallet/src/ui/components/transactions_tab.rs +++ b/applications/tari_console_wallet/src/ui/components/transactions_tab.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use chrono::{DateTime, Local}; use log::*; use tari_common_types::transaction::{TransactionDirection, TransactionStatus}; +use tari_wallet::transaction_service::storage::models::TxCancellationReason; use tokio::runtime::Handle; use tui::{ backend::Backend, @@ -96,13 +97,16 @@ impl TransactionsTab { let mut column2_items = Vec::new(); let mut column3_items = Vec::new(); for t in windowed_view.iter() { - let text_color = text_colors.get(&t.cancelled).unwrap_or(&Color::Reset).to_owned(); + let text_color = text_colors + .get(&t.cancelled.is_some()) + .unwrap_or(&Color::Reset) + .to_owned(); if t.direction == TransactionDirection::Outbound { column0_items.push(ListItem::new(Span::styled( app_state.get_alias(&t.destination_public_key), Style::default().fg(text_color), ))); - let amount_style = if t.cancelled { + let amount_style = if t.cancelled.is_some() { Style::default().fg(Color::Red).add_modifier(Modifier::DIM) } else { Style::default().fg(Color::Red) @@ -118,7 +122,7 @@ impl TransactionsTab { app_state.get_alias(&t.source_public_key), Style::default().fg(text_color), ))); - let amount_style = if t.cancelled { + let amount_style = if t.cancelled.is_some() { Style::default().fg(Color::Green).add_modifier(Modifier::DIM) } else { Style::default().fg(Color::Green) @@ -191,14 +195,14 @@ impl TransactionsTab { let mut column3_items = Vec::new(); for t in windowed_view.iter() { - let cancelled = t.cancelled || !t.valid; + let cancelled = t.cancelled.is_some(); let text_color = text_colors.get(&cancelled).unwrap_or(&Color::Reset).to_owned(); if t.direction == TransactionDirection::Outbound { column0_items.push(ListItem::new(Span::styled( app_state.get_alias(&t.destination_public_key), Style::default().fg(text_color), ))); - let amount_style = if t.cancelled { + let amount_style = if t.cancelled.is_some() { Style::default().fg(Color::Red).add_modifier(Modifier::DIM) } else { Style::default().fg(Color::Red) @@ -214,7 +218,7 @@ impl TransactionsTab { app_state.get_alias(&t.source_public_key), Style::default().fg(text_color), ))); - let color = match (t.cancelled, chain_height) { + let color = match (t.cancelled.is_some(), chain_height) { // cancelled (true, _) => Color::DarkGray, // not mature yet @@ -235,14 +239,12 @@ impl TransactionsTab { format!("{}", local_time.format("%Y-%m-%d %H:%M:%S")), Style::default().fg(text_color), ))); - let status = if (t.cancelled || !t.valid) && t.status == TransactionStatus::Coinbase { + let status = if matches!(t.cancelled, Some(TxCancellationReason::AbandonedCoinbase)) { "Abandoned".to_string() - } else if t.cancelled && t.status == TransactionStatus::Rejected { - "Rejected".to_string() - } else if t.cancelled { + } else if matches!(t.cancelled, Some(TxCancellationReason::UserCancelled)) { "Cancelled".to_string() - } else if !t.valid { - "Invalid".to_string() + } else if t.cancelled.is_some() { + "Rejected".to_string() } else { t.status.to_string() }; @@ -378,15 +380,12 @@ impl TransactionsTab { Span::styled(format!("{}", tx.fee), Style::default().fg(Color::White)), fee_details, ]); - let status_msg = if tx.cancelled && tx.status == TransactionStatus::Rejected { - "Rejected".to_string() - } else if tx.cancelled { - "Cancelled".to_string() - } else if !tx.valid { - "Invalid".to_string() + let status_msg = if let Some(reason) = tx.cancelled { + format!("Cancelled: {}", reason) } else { tx.status.to_string() }; + let status = Span::styled(status_msg, Style::default().fg(Color::White)); let message = Span::styled(tx.message.as_str(), Style::default().fg(Color::White)); let local_time = DateTime::::from_utc(tx.timestamp, Local::now().offset().to_owned()); @@ -396,9 +395,9 @@ impl TransactionsTab { ); let excess = Span::styled(tx.excess_signature.as_str(), Style::default().fg(Color::White)); let confirmation_count = app_state.get_confirmations(&tx.tx_id); - let confirmations_msg = if tx.status == TransactionStatus::MinedConfirmed && !tx.cancelled { + let confirmations_msg = if tx.status == TransactionStatus::MinedConfirmed && tx.cancelled.is_none() { format!("{} required confirmations met", required_confirmations) - } else if tx.status == TransactionStatus::MinedUnconfirmed && !tx.cancelled { + } else if tx.status == TransactionStatus::MinedUnconfirmed && tx.cancelled.is_none() { if let Some(count) = confirmation_count { format!("{} of {} required confirmations met", count, required_confirmations) } else { diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index 955762b728..06d0bd01d2 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -57,7 +57,10 @@ use tari_wallet::{ contacts_service::storage::database::Contact, output_manager_service::{handle::OutputManagerEventReceiver, service::Balance}, tokens::Token, - transaction_service::{handle::TransactionEventReceiver, storage::models::CompletedTransaction}, + transaction_service::{ + handle::TransactionEventReceiver, + storage::models::{CompletedTransaction, TxCancellationReason}, + }, WalletSqlite, }; use tokio::{ @@ -415,7 +418,7 @@ impl AppState { self.cached_data .completed_txs .iter() - .filter(|tx| !((tx.cancelled || !tx.valid) && tx.status == TransactionStatus::Coinbase)) + .filter(|tx| !matches!(tx.cancelled, Some(TxCancellationReason::AbandonedCoinbase))) .collect() } else { self.cached_data.completed_txs.iter().collect() @@ -697,14 +700,14 @@ impl AppStateInner { let tx = CompletedTransactionInfo::from_completed_transaction(tx.into(), &self.get_transaction_weight()); if let Some(index) = self.data.pending_txs.iter().position(|i| i.tx_id == tx_id) { - if tx.status == TransactionStatus::Pending && !tx.cancelled { + if tx.status == TransactionStatus::Pending && tx.cancelled.is_none() { self.data.pending_txs[index] = tx; self.updated = true; return Ok(()); } else { let _ = self.data.pending_txs.remove(index); } - } else if tx.status == TransactionStatus::Pending && !tx.cancelled { + } else if tx.status == TransactionStatus::Pending && tx.cancelled.is_none() { self.data.pending_txs.push(tx); self.data.pending_txs.sort_by(|a, b| { b.timestamp @@ -971,9 +974,8 @@ pub struct CompletedTransactionInfo { pub status: TransactionStatus, pub message: String, pub timestamp: NaiveDateTime, - pub cancelled: bool, + pub cancelled: Option, pub direction: TransactionDirection, - pub valid: bool, pub mined_height: Option, pub is_coinbase: bool, pub weight: u64, @@ -1032,7 +1034,6 @@ impl CompletedTransactionInfo { timestamp: tx.timestamp, cancelled: tx.cancelled, direction: tx.direction, - valid: tx.valid, mined_height: tx.mined_height, is_coinbase, weight, diff --git a/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/down.sql b/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/down.sql new file mode 100644 index 0000000000..291a97c5ce --- /dev/null +++ b/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/up.sql b/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/up.sql new file mode 100644 index 0000000000..e25d452b02 --- /dev/null +++ b/base_layer/wallet/migrations/2022-02-14-104456_cancellation_column_update/up.sql @@ -0,0 +1,60 @@ +PRAGMA foreign_keys=OFF; + +ALTER TABLE completed_transactions + RENAME TO completed_transactions_old; + +CREATE TABLE completed_transactions +( + tx_id BIGINT NOT NULL PRIMARY KEY, + source_public_key BLOB NOT NULL, + destination_public_key BLOB NOT NULL, + amount BIGINT NOT NULL, + fee BIGINT NOT NULL, + transaction_protocol TEXT NOT NULL, + status INTEGER NOT NULL, + message TEXT NOT NULL, + timestamp DATETIME NOT NULL, + cancelled INTEGER NULL, + direction INTEGER, + coinbase_block_height BIGINT, + send_count INTEGER default 0 NOT NULL, + last_send_timestamp DATETIME, + confirmations BIGINT default NULL, + mined_height BIGINT, + mined_in_block BLOB, + transaction_signature_nonce BLOB default 0 NOT NULL, + transaction_signature_key BLOB default 0 NOT NULL +); + +INSERT INTO completed_transactions (tx_id, source_public_key, destination_public_key, amount, fee, transaction_protocol, + status, message, timestamp, cancelled, direction, coinbase_block_height, send_count, + last_send_timestamp, confirmations, mined_height, mined_in_block, transaction_signature_nonce, + transaction_signature_key) +SELECT tx_id, + source_public_key, + destination_public_key, + amount, + fee, + transaction_protocol, + status, + message, + timestamp, + CASE completed_transactions_old.valid --This flag was only ever used to signify an abandoned coinbase, we will do that in the cancelled reason enum now + WHEN 0 + THEN 7 -- This is the value for AbandonedCoinbase + ELSE + NULLIF(cancelled, 0) + END, + direction, + coinbase_block_height, + send_count, + last_send_timestamp, + confirmations, + mined_height, + mined_in_block, + transaction_signature_nonce, + transaction_signature_key +FROM completed_transactions_old; + +DROP TABLE completed_transactions_old; +PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index 828fefeebb..06d17e3974 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -954,21 +954,22 @@ where )?; // Clear any existing pending coinbase transactions for this blockheight if they exist - if let Err(e) = self + match self .resources .db .clear_pending_coinbase_transaction_at_block_height(block_height) .await { - match e { - OutputManagerStorageError::DieselError(DieselError::NotFound) => { - debug!( - target: LOG_TARGET, - "An existing pending coinbase was cleared for block height {}", block_height - ) - }, + Ok(_) => { + debug!( + target: LOG_TARGET, + "An existing pending coinbase was cleared for block height {}", block_height + ) + }, + Err(e) => match e { + OutputManagerStorageError::DieselError(DieselError::NotFound) => {}, _ => return Err(OutputManagerError::from(e)), - } + }, }; // Clear any matching outputs for this commitment. Even if the older output is valid diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs index a93c6288e1..bc428e1516 100644 --- a/base_layer/wallet/src/schema.rs +++ b/base_layer/wallet/src/schema.rs @@ -16,12 +16,11 @@ table! { status -> Integer, message -> Text, timestamp -> Timestamp, - cancelled -> Integer, + cancelled -> Nullable, direction -> Nullable, coinbase_block_height -> Nullable, send_count -> Integer, last_send_timestamp -> Nullable, - valid -> Integer, confirmations -> Nullable, mined_height -> Nullable, mined_in_block -> Nullable, diff --git a/base_layer/wallet/src/transaction_service/error.rs b/base_layer/wallet/src/transaction_service/error.rs index 662807fb93..1351939a7a 100644 --- a/base_layer/wallet/src/transaction_service/error.rs +++ b/base_layer/wallet/src/transaction_service/error.rs @@ -222,6 +222,8 @@ pub enum TransactionStorageError { TransactionNotMined(TxId), #[error("Conversion error: `{0}`")] ByteArrayError(#[from] ByteArrayError), + #[error("Not a coinbase transaction so cannot be abandoned")] + NotCoinbase, } /// This error type is used to return TransactionServiceErrors from inside a Transaction Service protocol but also diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index 2df0115ec4..ff7d774934 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -40,8 +40,13 @@ use tower::Service; use crate::{ transaction_service::{ error::TransactionServiceError, - protocols::TxRejection, - storage::models::{CompletedTransaction, InboundTransaction, OutboundTransaction, WalletTransaction}, + storage::models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TxCancellationReason, + WalletTransaction, + }, }, OperationId, }; @@ -208,7 +213,7 @@ pub enum TransactionEvent { TransactionDirectSendResult(TxId, bool), TransactionCompletedImmediately(TxId), TransactionStoreForwardSendResult(TxId, bool), - TransactionCancelled(TxId, TxRejection), + TransactionCancelled(TxId, TxCancellationReason), TransactionBroadcast(TxId), TransactionImported(TxId), FauxTransactionUnconfirmed { diff --git a/base_layer/wallet/src/transaction_service/protocols/mod.rs b/base_layer/wallet/src/transaction_service/protocols/mod.rs index 664aab6146..15bdb1dd1c 100644 --- a/base_layer/wallet/src/transaction_service/protocols/mod.rs +++ b/base_layer/wallet/src/transaction_service/protocols/mod.rs @@ -20,17 +20,6 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum TxRejection { - Unknown, // 0 - UserCancelled, // 1 - Timeout, // 2 - DoubleSpend, // 3 - Orphan, // 4 - TimeLocked, // 5 - InvalidTransaction, // 6 -} - pub mod transaction_broadcast_protocol; pub mod transaction_receive_protocol; pub mod transaction_send_protocol; diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs index 6674c0e8bf..967327b2a1 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_broadcast_protocol.rs @@ -47,9 +47,11 @@ use crate::{ transaction_service::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, - protocols::TxRejection, service::TransactionServiceResources, - storage::{database::TransactionBackend, models::CompletedTransaction}, + storage::{ + database::TransactionBackend, + models::{CompletedTransaction, TxCancellationReason}, + }, }, }; @@ -218,33 +220,35 @@ where "Transaction (TxId: {}) rejected by Base Node for reason: {}", self.tx_id, response.rejection_reason ); - self.cancel_transaction().await; - - let reason = match response.rejection_reason { - TxSubmissionRejectionReason::None | TxSubmissionRejectionReason::ValidationFailed => { - TransactionServiceError::MempoolRejectionInvalidTransaction - }, - TxSubmissionRejectionReason::DoubleSpend => TransactionServiceError::MempoolRejectionDoubleSpend, - TxSubmissionRejectionReason::Orphan => TransactionServiceError::MempoolRejectionOrphan, - TxSubmissionRejectionReason::TimeLocked => TransactionServiceError::MempoolRejectionTimeLocked, - _ => TransactionServiceError::UnexpectedBaseNodeResponse, + let (reason_error, reason) = match response.rejection_reason { + TxSubmissionRejectionReason::None | TxSubmissionRejectionReason::ValidationFailed => ( + TransactionServiceError::MempoolRejectionInvalidTransaction, + TxCancellationReason::InvalidTransaction, + ), + TxSubmissionRejectionReason::DoubleSpend => ( + TransactionServiceError::MempoolRejectionDoubleSpend, + TxCancellationReason::DoubleSpend, + ), + TxSubmissionRejectionReason::Orphan => ( + TransactionServiceError::MempoolRejectionOrphan, + TxCancellationReason::Orphan, + ), + TxSubmissionRejectionReason::TimeLocked => ( + TransactionServiceError::MempoolRejectionTimeLocked, + TxCancellationReason::TimeLocked, + ), + _ => ( + TransactionServiceError::UnexpectedBaseNodeResponse, + TxCancellationReason::Unknown, + ), }; - let cancellation_event_reason = match reason { - TransactionServiceError::MempoolRejectionInvalidTransaction => TxRejection::InvalidTransaction, - TransactionServiceError::MempoolRejectionDoubleSpend => TxRejection::DoubleSpend, - TransactionServiceError::MempoolRejectionOrphan => TxRejection::Orphan, - TransactionServiceError::MempoolRejectionTimeLocked => TxRejection::TimeLocked, - _ => TxRejection::Unknown, - }; + self.cancel_transaction(reason).await; let _ = self .resources .event_publisher - .send(Arc::new(TransactionEvent::TransactionCancelled( - self.tx_id, - cancellation_event_reason, - ))) + .send(Arc::new(TransactionEvent::TransactionCancelled(self.tx_id, reason))) .map_err(|e| { trace!( target: LOG_TARGET, @@ -254,7 +258,7 @@ where e }); - return Err(TransactionServiceProtocolError::new(self.tx_id, reason)); + return Err(TransactionServiceProtocolError::new(self.tx_id, reason_error)); } else if response.rejection_reason == TxSubmissionRejectionReason::AlreadyMined { info!( target: LOG_TARGET, @@ -354,14 +358,14 @@ where cancelling transaction", self.tx_id ); - self.cancel_transaction().await; + self.cancel_transaction(TxCancellationReason::InvalidTransaction).await; let _ = self .resources .event_publisher .send(Arc::new(TransactionEvent::TransactionCancelled( self.tx_id, - TxRejection::InvalidTransaction, + TxCancellationReason::InvalidTransaction, ))) .map_err(|e| { trace!( @@ -413,7 +417,7 @@ where } } - async fn cancel_transaction(&mut self) { + async fn cancel_transaction(&mut self, reason: TxCancellationReason) { if let Err(e) = self .resources .output_manager_service @@ -425,7 +429,7 @@ where "Failed to Cancel outputs for TxId: {} after failed sending attempt with error {:?}", self.tx_id, e ); } - if let Err(e) = self.resources.db.reject_completed_transaction(self.tx_id).await { + if let Err(e) = self.resources.db.reject_completed_transaction(self.tx_id, reason).await { warn!( target: LOG_TARGET, "Failed to Cancel TxId: {} after failed sending attempt with error {:?}", self.tx_id, e diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs index d3df9907e6..c51020b37c 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_receive_protocol.rs @@ -45,11 +45,10 @@ use crate::{ transaction_service::{ error::{TransactionServiceError, TransactionServiceProtocolError}, handle::TransactionEvent, - protocols::TxRejection, service::TransactionServiceResources, storage::{ database::TransactionBackend, - models::{CompletedTransaction, InboundTransaction}, + models::{CompletedTransaction, InboundTransaction, TxCancellationReason}, }, tasks::send_transaction_reply::send_transaction_reply, utc::utc_duration_since, @@ -509,7 +508,7 @@ where .event_publisher .send(Arc::new(TransactionEvent::TransactionCancelled( self.id, - TxRejection::Timeout, + TxCancellationReason::Timeout, ))) .map_err(|e| { trace!( diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs index 59819d5d85..a83fa45811 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_send_protocol.rs @@ -60,11 +60,10 @@ use crate::{ config::TransactionRoutingMechanism, error::{TransactionServiceError, TransactionServiceProtocolError}, handle::{TransactionEvent, TransactionServiceResponse}, - protocols::TxRejection, service::TransactionServiceResources, storage::{ database::TransactionBackend, - models::{CompletedTransaction, OutboundTransaction}, + models::{CompletedTransaction, OutboundTransaction, TxCancellationReason}, }, tasks::{ send_finalized_transaction::send_finalized_transaction_message, @@ -806,7 +805,7 @@ where .event_publisher .send(Arc::new(TransactionEvent::TransactionCancelled( self.id, - TxRejection::Timeout, + TxCancellationReason::Timeout, ))) .map_err(|e| { trace!( diff --git a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs index 3572317408..0552d2263f 100644 --- a/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs +++ b/base_layer/wallet/src/transaction_service/protocols/transaction_validation_protocol.rs @@ -69,7 +69,7 @@ pub struct TransactionValidationProtocol TransactionValidationProtocol @@ -284,7 +284,6 @@ where > { let mut mined = vec![]; let mut unmined = vec![]; - let mut batch_signatures = HashMap::new(); for tx_info in batch.iter() { // Imported transactions do not have a signature; this is represented by the default signature in info @@ -391,7 +390,6 @@ where self.db .set_transaction_mined_height( tx_id, - true, mined_height, mined_in_block.clone(), num_confirmations, @@ -447,7 +445,6 @@ where self.db .set_transaction_mined_height( tx_id, - false, mined_height, mined_in_block.clone(), num_confirmations, @@ -457,6 +454,11 @@ where .await .for_protocol(self.operation_id.as_u64())?; + self.db + .abandon_coinbase_transaction(tx_id) + .await + .for_protocol(self.operation_id.as_u64())?; + if let Err(e) = self.output_manager_handle.set_coinbase_abandoned(tx_id, true).await { warn!( target: LOG_TARGET, @@ -466,9 +468,10 @@ where self.operation_id ); }; - - self.publish_event(TransactionEvent::TransactionCancelled(tx_id, TxRejection::Orphan)); - + self.publish_event(TransactionEvent::TransactionCancelled( + tx_id, + TxCancellationReason::AbandonedCoinbase, + )); Ok(()) } diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 3793454b73..32a0bfd39a 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -86,11 +86,10 @@ use crate::{ transaction_receive_protocol::{TransactionReceiveProtocol, TransactionReceiveProtocolStage}, transaction_send_protocol::{TransactionSendProtocol, TransactionSendProtocolStage}, transaction_validation_protocol::TransactionValidationProtocol, - TxRejection, }, storage::{ database::{TransactionBackend, TransactionDatabase}, - models::CompletedTransaction, + models::{CompletedTransaction, TxCancellationReason}, }, tasks::{ check_faux_transaction_status::check_faux_transactions, @@ -1244,7 +1243,7 @@ where return Ok(()); } - if ctx.cancelled { + if ctx.cancelled.is_some() { // Send a cancellation message debug!( target: LOG_TARGET, @@ -1408,7 +1407,7 @@ where .event_publisher .send(Arc::new(TransactionEvent::TransactionCancelled( tx_id, - TxRejection::UserCancelled, + TxCancellationReason::UserCancelled, ))) .map_err(|e| { trace!( diff --git a/base_layer/wallet/src/transaction_service/storage/database.rs b/base_layer/wallet/src/transaction_service/storage/database.rs index c51bdaf963..a3af2fd1d2 100644 --- a/base_layer/wallet/src/transaction_service/storage/database.rs +++ b/base_layer/wallet/src/transaction_service/storage/database.rs @@ -41,7 +41,13 @@ use tari_core::transactions::{tari_amount::MicroTari, transaction_components::Tr use crate::transaction_service::{ error::TransactionStorageError, storage::{ - models::{CompletedTransaction, InboundTransaction, OutboundTransaction, WalletTransaction}, + models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TxCancellationReason, + WalletTransaction, + }, sqlite_db::{InboundTransactionSenderInfo, UnconfirmedTransactionInfo}, }, }; @@ -92,7 +98,11 @@ pub trait TransactionBackend: Send + Sync + Clone { /// Indicated that a completed transaction has been broadcast to the mempools fn broadcast_completed_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; /// Cancel Completed transaction, this will update the transaction status - fn reject_completed_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; + fn reject_completed_transaction( + &self, + tx_id: TxId, + reason: TxCancellationReason, + ) -> Result<(), TransactionStorageError>; /// Set cancellation on Pending transaction, this will update the transaction status fn set_pending_transaction_cancellation_status( &self, @@ -128,7 +138,6 @@ pub trait TransactionBackend: Send + Sync + Clone { fn update_mined_height( &self, tx_id: TxId, - is_valid: bool, mined_height: u64, mined_in_block: BlockHash, num_confirmations: u64, @@ -149,6 +158,7 @@ pub trait TransactionBackend: Send + Sync + Clone { &self, height: u64, ) -> Result, TransactionStorageError>; + fn abandon_coinbase_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError>; } #[derive(Clone, PartialEq)] @@ -427,7 +437,7 @@ where T: TransactionBackend + 'static let t = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::CompletedTransaction(tx_id)) { Ok(None) => Err(TransactionStorageError::ValueNotFound(key)), Ok(Some(DbValue::CompletedTransaction(pt))) => { - if pt.cancelled == cancelled { + if (pt.cancelled.is_some()) == cancelled { Ok(pt) } else { Err(TransactionStorageError::ValueNotFound(key)) @@ -687,9 +697,13 @@ where T: TransactionBackend + 'static .and_then(|inner_result| inner_result) } - pub async fn reject_completed_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + pub async fn reject_completed_transaction( + &self, + tx_id: TxId, + reason: TxCancellationReason, + ) -> Result<(), TransactionStorageError> { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || db_clone.reject_completed_transaction(tx_id)) + tokio::task::spawn_blocking(move || db_clone.reject_completed_transaction(tx_id, reason)) .await .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) @@ -842,7 +856,6 @@ where T: TransactionBackend + 'static pub async fn set_transaction_mined_height( &self, tx_id: TxId, - is_valid: bool, mined_height: u64, mined_in_block: BlockHash, num_confirmations: u64, @@ -853,7 +866,6 @@ where T: TransactionBackend + 'static tokio::task::spawn_blocking(move || { db_clone.update_mined_height( tx_id, - is_valid, mined_height, mined_in_block, num_confirmations, @@ -879,6 +891,14 @@ where T: TransactionBackend + 'static .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(t) } + + pub async fn abandon_coinbase_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + let db_clone = self.db.clone(); + tokio::task::spawn_blocking(move || db_clone.abandon_coinbase_transaction(tx_id)) + .await + .map_err(|err| TransactionStorageError::BlockingTaskSpawnError(err.to_string()))??; + Ok(()) + } } impl Display for DbKey { diff --git a/base_layer/wallet/src/transaction_service/storage/models.rs b/base_layer/wallet/src/transaction_service/storage/models.rs index 62febbe9b0..fbfb0638ca 100644 --- a/base_layer/wallet/src/transaction_service/storage/models.rs +++ b/base_layer/wallet/src/transaction_service/storage/models.rs @@ -20,10 +20,15 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use std::{ + convert::TryFrom, + fmt::{Display, Error, Formatter}, +}; + use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use tari_common_types::{ - transaction::{TransactionDirection, TransactionStatus, TxId}, + transaction::{TransactionConversionError, TransactionDirection, TransactionStatus, TxId}, types::{BlockHash, PrivateKey, Signature}, }; use tari_comms::types::CommsPublicKey; @@ -133,12 +138,11 @@ pub struct CompletedTransaction { pub status: TransactionStatus, pub message: String, pub timestamp: NaiveDateTime, - pub cancelled: bool, + pub cancelled: Option, pub direction: TransactionDirection, pub coinbase_block_height: Option, pub send_count: u32, pub last_send_timestamp: Option, - pub valid: bool, pub transaction_signature: Signature, pub confirmations: Option, pub mined_height: Option, @@ -176,12 +180,11 @@ impl CompletedTransaction { status, message, timestamp, - cancelled: false, + cancelled: None, direction, coinbase_block_height, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature, confirmations: None, mined_height, @@ -225,7 +228,7 @@ impl From for InboundTransaction { status: ct.status, message: ct.message, timestamp: ct.timestamp, - cancelled: ct.cancelled, + cancelled: ct.cancelled.is_some(), direct_send_success: false, send_count: 0, last_send_timestamp: None, @@ -244,7 +247,7 @@ impl From for OutboundTransaction { status: ct.status, message: ct.message, timestamp: ct.timestamp, - cancelled: ct.cancelled, + cancelled: ct.cancelled.is_some(), direct_send_success: false, send_count: 0, last_send_timestamp: None, @@ -276,13 +279,16 @@ impl From for CompletedTransaction { status: tx.status, message: tx.message, timestamp: tx.timestamp, - cancelled: tx.cancelled, + cancelled: if tx.cancelled { + Some(TxCancellationReason::UserCancelled) + } else { + None + }, transaction, direction: TransactionDirection::Outbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature, confirmations: None, mined_height: None, @@ -302,13 +308,16 @@ impl From for CompletedTransaction { status: tx.status, message: tx.message, timestamp: tx.timestamp, - cancelled: tx.cancelled, + cancelled: if tx.cancelled { + Some(TxCancellationReason::UserCancelled) + } else { + None + }, transaction: Transaction::new(vec![], vec![], vec![], PrivateKey::default(), PrivateKey::default()), direction: TransactionDirection::Inbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: Signature::default(), confirmations: None, mined_height: None, @@ -334,3 +343,50 @@ impl From for CompletedTransaction { } } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum TxCancellationReason { + Unknown, // 0 + UserCancelled, // 1 + Timeout, // 2 + DoubleSpend, // 3 + Orphan, // 4 + TimeLocked, // 5 + InvalidTransaction, // 6 + AbandonedCoinbase, // 7 +} + +impl TryFrom for TxCancellationReason { + type Error = TransactionConversionError; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(TxCancellationReason::Unknown), + 1 => Ok(TxCancellationReason::UserCancelled), + 2 => Ok(TxCancellationReason::Timeout), + 3 => Ok(TxCancellationReason::DoubleSpend), + 4 => Ok(TxCancellationReason::Orphan), + 5 => Ok(TxCancellationReason::TimeLocked), + 6 => Ok(TxCancellationReason::InvalidTransaction), + 7 => Ok(TxCancellationReason::AbandonedCoinbase), + code => Err(TransactionConversionError { code: code as i32 }), + } + } +} + +impl Display for TxCancellationReason { + fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), Error> { + use TxCancellationReason::*; + let response = match self { + Unknown => "Unknown", + UserCancelled => "User Cancelled", + Timeout => "Timeout", + DoubleSpend => "Double Spend", + Orphan => "Orphan", + TimeLocked => "TimeLocked", + InvalidTransaction => "Invalid Transaction", + AbandonedCoinbase => "Abandoned Coinbase", + }; + fmt.write_str(response) + } +} diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index 9076b1c259..06af0ed5f0 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -57,7 +57,13 @@ use crate::{ error::{TransactionKeyError, TransactionStorageError}, storage::{ database::{DbKey, DbKeyValuePair, DbValue, TransactionBackend, WriteOperation}, - models::{CompletedTransaction, InboundTransaction, OutboundTransaction, WalletTransaction}, + models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TxCancellationReason, + WalletTransaction, + }, }, }, util::{ @@ -667,13 +673,17 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { Ok(()) } - fn reject_completed_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + fn reject_completed_transaction( + &self, + tx_id: TxId, + reason: TxCancellationReason, + ) -> Result<(), TransactionStorageError> { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); match CompletedTransactionSql::find_by_cancelled(tx_id, false, &conn) { Ok(v) => { - v.reject(&conn)?; + v.reject(reason, &conn)?; }, Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { return Err(TransactionStorageError::ValueNotFound(DbKey::CompletedTransaction( @@ -910,7 +920,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let coinbase_txs = CompletedTransactionSql::index_coinbase_at_block_height(block_height as i64, &conn)?; for c in coinbase_txs.iter() { - c.cancel(&conn)?; + c.reject(TxCancellationReason::AbandonedCoinbase, &conn)?; } if start.elapsed().as_millis() > 0 { trace!( @@ -1004,7 +1014,6 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { fn update_mined_height( &self, tx_id: TxId, - is_valid: bool, mined_height: u64, mined_in_block: BlockHash, num_confirmations: u64, @@ -1017,7 +1026,6 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { match CompletedTransactionSql::find(tx_id, &conn) { Ok(v) => { v.update_mined_height( - is_valid, mined_height, mined_in_block, num_confirmations, @@ -1108,7 +1116,6 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); let txs = completed_transactions::table - .filter(completed_transactions::valid.eq(true as i32)) .filter( completed_transactions::status .eq(TransactionStatus::Completed as i32) @@ -1119,7 +1126,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { .is_null() .or(completed_transactions::coinbase_block_height.eq(0)), ) - .filter(completed_transactions::cancelled.eq(false as i32)) + .filter(completed_transactions::cancelled.is_null()) .order_by(completed_transactions::tx_id) .load::(&*conn)?; @@ -1145,7 +1152,7 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { let start = Instant::now(); let conn = self.database_connection.get_pooled_connection()?; let acquire_lock = start.elapsed(); - let result = diesel::update(completed_transactions::table.filter(completed_transactions::cancelled.eq(0))) + let result = diesel::update(completed_transactions::table.filter(completed_transactions::cancelled.is_null())) .set(( completed_transactions::mined_height.eq::>(None), completed_transactions::mined_in_block.eq::>>(None), @@ -1265,6 +1272,23 @@ impl TransactionBackend for TransactionServiceSqliteDatabase { }) .collect::, TransactionStorageError>>() } + + fn abandon_coinbase_transaction(&self, tx_id: TxId) -> Result<(), TransactionStorageError> { + let conn = self.database_connection.get_pooled_connection()?; + match CompletedTransactionSql::find_by_cancelled(tx_id, false, &conn) { + Ok(tx) => { + tx.abandon_coinbase(&conn)?; + }, + Err(TransactionStorageError::DieselError(DieselError::NotFound)) => { + return Err(TransactionStorageError::ValueNotFound(DbKey::CompletedTransaction( + tx_id, + ))); + }, + Err(e) => return Err(e), + }; + + Ok(()) + } } #[derive(Debug, PartialEq)] @@ -1668,12 +1692,11 @@ struct CompletedTransactionSql { status: i32, message: String, timestamp: NaiveDateTime, - cancelled: i32, + cancelled: Option, direction: Option, coinbase_block_height: Option, send_count: i32, last_send_timestamp: Option, - valid: i32, confirmations: Option, mined_height: Option, mined_in_block: Option>, @@ -1697,9 +1720,15 @@ impl CompletedTransactionSql { conn: &SqliteConnection, cancelled: bool, ) -> Result, TransactionStorageError> { - Ok(completed_transactions::table - .filter(completed_transactions::cancelled.eq(cancelled as i32)) - .load::(conn)?) + let mut query = completed_transactions::table.into_boxed(); + + query = if cancelled { + query.filter(completed_transactions::cancelled.is_not_null()) + } else { + query.filter(completed_transactions::cancelled.is_null()) + }; + + Ok(query.load::(conn)?) } pub fn index_by_status_and_cancelled( @@ -1707,8 +1736,13 @@ impl CompletedTransactionSql { cancelled: bool, conn: &SqliteConnection, ) -> Result, TransactionStorageError> { - Ok(completed_transactions::table - .filter(completed_transactions::cancelled.eq(cancelled as i32)) + let mut query = completed_transactions::table.into_boxed(); + query = if cancelled { + query.filter(completed_transactions::cancelled.is_not_null()) + } else { + query.filter(completed_transactions::cancelled.is_null()) + }; + Ok(query .filter(completed_transactions::status.eq(status as i32)) .load::(conn)?) } @@ -1719,8 +1753,14 @@ impl CompletedTransactionSql { block_height: i64, conn: &SqliteConnection, ) -> Result, TransactionStorageError> { - Ok(completed_transactions::table - .filter(completed_transactions::cancelled.eq(cancelled as i32)) + let mut query = completed_transactions::table.into_boxed(); + query = if cancelled { + query.filter(completed_transactions::cancelled.is_not_null()) + } else { + query.filter(completed_transactions::cancelled.is_null()) + }; + + Ok(query .filter(completed_transactions::status.eq(status as i32)) .filter(completed_transactions::mined_height.ge(block_height)) .load::(conn)?) @@ -1747,10 +1787,17 @@ impl CompletedTransactionSql { cancelled: bool, conn: &SqliteConnection, ) -> Result { - Ok(completed_transactions::table + let mut query = completed_transactions::table .filter(completed_transactions::tx_id.eq(tx_id.as_u64() as i64)) - .filter(completed_transactions::cancelled.eq(cancelled as i32)) - .first::(conn)?) + .into_boxed(); + + query = if cancelled { + query.filter(completed_transactions::cancelled.is_not_null()) + } else { + query.filter(completed_transactions::cancelled.is_null()) + }; + + Ok(query.first::(conn)?) } pub fn delete(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { @@ -1777,10 +1824,10 @@ impl CompletedTransactionSql { Ok(()) } - pub fn reject(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { + pub fn reject(&self, reason: TxCancellationReason, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { self.update( UpdateCompletedTransactionSql { - cancelled: Some(1i32), + cancelled: Some(Some(reason as i32)), status: Some(TransactionStatus::Rejected as i32), ..Default::default() }, @@ -1790,10 +1837,14 @@ impl CompletedTransactionSql { Ok(()) } - pub fn cancel(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { + pub fn abandon_coinbase(&self, conn: &SqliteConnection) -> Result<(), TransactionStorageError> { + if self.coinbase_block_height.is_none() { + return Err(TransactionStorageError::NotCoinbase); + } + self.update( UpdateCompletedTransactionSql { - cancelled: Some(1i32), + cancelled: Some(Some(TxCancellationReason::AbandonedCoinbase as i32)), ..Default::default() }, conn, @@ -1819,8 +1870,8 @@ impl CompletedTransactionSql { mined_in_block: Some(None), mined_height: Some(None), confirmations: Some(None), - // Resets to valid - valid: Some(1), + // Turns out it should not be cancelled + cancelled: Some(None), ..Default::default() }, conn, @@ -1846,7 +1897,6 @@ impl CompletedTransactionSql { pub fn update_mined_height( &self, - is_valid: bool, mined_height: u64, mined_in_block: BlockHash, num_confirmations: u64, @@ -1854,9 +1904,7 @@ impl CompletedTransactionSql { conn: &SqliteConnection, is_faux: bool, ) -> Result<(), TransactionStorageError> { - let status = if self.coinbase_block_height.is_some() && !is_valid { - TransactionStatus::Coinbase as i32 - } else if is_confirmed { + let status = if is_confirmed { if is_faux { TransactionStatus::FauxConfirmed as i32 } else { @@ -1874,9 +1922,8 @@ impl CompletedTransactionSql { status: Some(status), mined_height: Some(Some(mined_height as i64)), mined_in_block: Some(Some(mined_in_block)), - valid: Some(is_valid as i32), // If the tx is mined, then it can't be cancelled - cancelled: Some(0), + cancelled: None, ..Default::default() }, conn, @@ -1919,12 +1966,11 @@ impl TryFrom for CompletedTransactionSql { status: c.status as i32, message: c.message, timestamp: c.timestamp, - cancelled: c.cancelled as i32, + cancelled: c.cancelled.map(|v| v as i32), direction: Some(c.direction as i32), coinbase_block_height: c.coinbase_block_height.map(|b| b as i64), send_count: c.send_count as i32, last_send_timestamp: c.last_send_timestamp, - valid: c.valid as i32, confirmations: c.confirmations.map(|ic| ic as i64), mined_height: c.mined_height.map(|ic| ic as i64), mined_in_block: c.mined_in_block, @@ -1968,12 +2014,13 @@ impl TryFrom for CompletedTransaction { status: TransactionStatus::try_from(c.status)?, message: c.message, timestamp: c.timestamp, - cancelled: c.cancelled != 0, + cancelled: c + .cancelled + .map(|v| TxCancellationReason::try_from(v as u32).unwrap_or(TxCancellationReason::Unknown)), direction: TransactionDirection::try_from(c.direction.unwrap_or(2i32))?, coinbase_block_height: c.coinbase_block_height.map(|b| b as u64), send_count: c.send_count as u32, last_send_timestamp: c.last_send_timestamp, - valid: c.valid != 0, transaction_signature, confirmations: c.confirmations.map(|ic| ic as u64), mined_height: c.mined_height.map(|ic| ic as u64), @@ -1987,12 +2034,11 @@ impl TryFrom for CompletedTransaction { pub struct UpdateCompletedTransactionSql { status: Option, timestamp: Option, - cancelled: Option, + cancelled: Option>, direction: Option, transaction_protocol: Option, send_count: Option, last_send_timestamp: Option>, - valid: Option, confirmations: Option>, mined_height: Option>, mined_in_block: Option>>, @@ -2068,7 +2114,7 @@ impl UnconfirmedTransactionInfoSql { .or(completed_transactions::status.eq(TransactionStatus::MinedUnconfirmed as i32)), ), ) - .filter(completed_transactions::cancelled.eq(false as i32)) + .filter(completed_transactions::cancelled.is_null()) .order_by(completed_transactions::tx_id) .load::(&*conn)?; Ok(query_result) @@ -2116,7 +2162,7 @@ mod test { test_utils::create_consensus_constants, transaction_service::storage::{ database::{DbKey, TransactionBackend}, - models::{CompletedTransaction, InboundTransaction, OutboundTransaction}, + models::{CompletedTransaction, InboundTransaction, OutboundTransaction, TxCancellationReason}, sqlite_db::{ CompletedTransactionSql, InboundTransactionSenderInfo, @@ -2299,12 +2345,11 @@ mod test { status: TransactionStatus::MinedUnconfirmed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -2320,12 +2365,11 @@ mod test { status: TransactionStatus::Broadcast, message: "Hey!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -2435,7 +2479,7 @@ mod test { assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, true, &conn).is_err()); CompletedTransactionSql::try_from(completed_tx1.clone()) .unwrap() - .reject(&conn) + .reject(TxCancellationReason::Unknown, &conn) .unwrap(); assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, false, &conn).is_err()); assert!(CompletedTransactionSql::find_by_cancelled(completed_tx1.tx_id, true, &conn).is_ok()); @@ -2450,12 +2494,11 @@ mod test { status: TransactionStatus::Coinbase, message: "Hey!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: Some(2), send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -2472,12 +2515,11 @@ mod test { status: TransactionStatus::Coinbase, message: "Hey!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: Some(2), send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -2494,12 +2536,11 @@ mod test { status: TransactionStatus::Coinbase, message: "Hey!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: Some(3), send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -2606,12 +2647,11 @@ mod test { status: TransactionStatus::MinedUnconfirmed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: Signature::default(), confirmations: None, mined_height: None, @@ -2697,12 +2737,11 @@ mod test { status: TransactionStatus::MinedUnconfirmed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Unknown, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: Signature::default(), confirmations: None, mined_height: None, @@ -2760,19 +2799,51 @@ mod test { let mut info_list_reference: Vec = vec![]; for i in 0..1000 { - let (valid, cancelled, status, coinbase_block_height) = match i % 13 { - 0 => (true, i % 3 == 0, TransactionStatus::Completed, None), - 1 => (true, i % 5 == 0, TransactionStatus::Broadcast, None), - 2 => (true, i % 7 == 0, TransactionStatus::Completed, Some(i % 2)), - 3 => (true, i % 11 == 0, TransactionStatus::Broadcast, Some(i % 2)), - 4 => (i % 13 == 0, false, TransactionStatus::Completed, None), - 5 => (i % 17 == 0, false, TransactionStatus::Broadcast, None), - 6 => (true, false, TransactionStatus::Pending, None), - 7 => (true, false, TransactionStatus::Coinbase, None), - 8 => (true, false, TransactionStatus::MinedUnconfirmed, None), - 9 => (true, false, TransactionStatus::Imported, None), - 10 => (true, false, TransactionStatus::MinedConfirmed, None), - _ => (true, false, TransactionStatus::Completed, Some(i)), + let (cancelled, status, coinbase_block_height) = match i % 13 { + 0 => ( + if i % 3 == 0 { + Some(TxCancellationReason::Unknown) + } else { + None + }, + TransactionStatus::Completed, + None, + ), + 1 => ( + if i % 5 == 0 { + Some(TxCancellationReason::Unknown) + } else { + None + }, + TransactionStatus::Broadcast, + None, + ), + 2 => ( + if i % 7 == 0 { + Some(TxCancellationReason::Unknown) + } else { + None + }, + TransactionStatus::Completed, + Some(i % 2), + ), + 3 => ( + if i % 11 == 0 { + Some(TxCancellationReason::Unknown) + } else { + None + }, + TransactionStatus::Broadcast, + Some(i % 2), + ), + 4 => (None, TransactionStatus::Completed, None), + 5 => (None, TransactionStatus::Broadcast, None), + 6 => (None, TransactionStatus::Pending, None), + 7 => (None, TransactionStatus::Coinbase, None), + 8 => (None, TransactionStatus::MinedUnconfirmed, None), + 9 => (None, TransactionStatus::Imported, None), + 10 => (None, TransactionStatus::MinedConfirmed, None), + _ => (None, TransactionStatus::Completed, Some(i)), }; let completed_tx = CompletedTransaction { tx_id: TxId::from(i), @@ -2795,7 +2866,6 @@ mod test { coinbase_block_height, send_count: 0, last_send_timestamp: None, - valid, transaction_signature: Signature::default(), confirmations: None, mined_height: None, @@ -2808,7 +2878,7 @@ mod test { let inbound_tx_sql = InboundTransactionSql::try_from(inbound_tx.clone()).unwrap(); inbound_tx_sql.commit(&conn).unwrap(); - if !cancelled { + if cancelled.is_none() { info_list_reference.push(InboundTransactionSenderInfo { tx_id: inbound_tx.tx_id, source_public_key: inbound_tx.source_public_key, @@ -2820,11 +2890,10 @@ mod test { let db1 = TransactionServiceSqliteDatabase::new(connection, None); let txn_list = db1.get_transactions_to_be_broadcast().unwrap(); - assert_eq!(txn_list.len(), 185); + assert_eq!(txn_list.len(), 335); for txn in &txn_list { assert!(txn.status == TransactionStatus::Completed || txn.status == TransactionStatus::Broadcast); - assert!(txn.valid); - assert!(!txn.cancelled); + assert!(txn.cancelled.is_none()); assert!(txn.coinbase_block_height == None || txn.coinbase_block_height == Some(0)); } diff --git a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs index 6a5a776fe4..370a0c941f 100644 --- a/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs +++ b/base_layer/wallet/src/transaction_service/tasks/check_faux_transaction_status.rs @@ -128,7 +128,6 @@ pub async fn check_faux_transactions( let result = db .set_transaction_mined_height( tx.tx_id, - true, mined_height, mined_in_block, num_confirmations, diff --git a/base_layer/wallet/tests/transaction_service_tests/service.rs b/base_layer/wallet/tests/transaction_service_tests/service.rs index 45f7add1ba..c3c2705591 100644 --- a/base_layer/wallet/tests/transaction_service_tests/service.rs +++ b/base_layer/wallet/tests/transaction_service_tests/service.rs @@ -1937,12 +1937,11 @@ fn test_power_mode_updates() { status: TransactionStatus::Completed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Outbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -1959,12 +1958,11 @@ fn test_power_mode_updates() { status: TransactionStatus::Completed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Outbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -3486,11 +3484,9 @@ fn test_coinbase_generation_and_monitoring() { let tx = completed_txs.get(&tx_id1).unwrap(); assert_eq!(tx.status, TransactionStatus::Coinbase); - assert!(tx.valid); let tx = completed_txs.get(&tx_id2b).unwrap(); assert_eq!(tx.status, TransactionStatus::MinedUnconfirmed); - assert!(tx.valid); // Now we will have tx_id2b becoming confirmed let _ = transaction_query_batch_responses.pop(); @@ -3536,7 +3532,6 @@ fn test_coinbase_generation_and_monitoring() { let tx = completed_txs.get(&tx_id2b).unwrap(); assert_eq!(tx.status, TransactionStatus::MinedConfirmed); - assert!(tx.valid); } #[test] @@ -3548,9 +3543,6 @@ fn test_coinbase_abandoned() { let mut alice_ts_interface = setup_transaction_service_no_comms(&mut runtime, factories, connection, None); let mut alice_event_stream = alice_ts_interface.transaction_service_handle.get_event_stream(); - alice_ts_interface - .base_node_rpc_mock_state - .set_response_delay(Some(Duration::from_secs(1))); let block_height_a = 10; @@ -3643,15 +3635,14 @@ fn test_coinbase_abandoned() { assert_eq!(count, 1, "Expected a TransactionCancelled event"); }); - let tx = runtime + let txs = runtime .block_on( alice_ts_interface .transaction_service_handle - .get_completed_transaction(tx_id1), + .get_cancelled_completed_transactions(), ) .unwrap(); - assert_eq!(tx.status, TransactionStatus::Coinbase); - assert!(!tx.valid); + assert!(txs.get(&tx_id1).is_some()); let balance = runtime .block_on(alice_ts_interface.output_manager_service_handle.get_balance()) @@ -3687,7 +3678,7 @@ fn test_coinbase_abandoned() { .get_completed_transactions(), ) .unwrap(); - assert_eq!(transactions.len(), 2); + assert_eq!(transactions.len(), 1); let tx_id2 = transactions .values() .find(|tx| tx.amount == fees2 + reward2) @@ -3701,13 +3692,22 @@ fn test_coinbase_abandoned() { fees2 + reward2 ); - let transaction_query_batch_responses = vec![TxQueryBatchResponseProto { - signature: Some(SignatureProto::from(tx2.first_kernel_excess_sig().unwrap().clone())), - location: TxLocationProto::from(TxLocation::Mined) as i32, - block_hash: Some([11u8; 16].to_vec()), - confirmations: 2, - block_height: block_height_b, - }]; + let transaction_query_batch_responses = vec![ + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx1.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::NotStored) as i32, + block_hash: None, + confirmations: 0, + block_height: 0, + }, + TxQueryBatchResponseProto { + signature: Some(SignatureProto::from(tx2.first_kernel_excess_sig().unwrap().clone())), + location: TxLocationProto::from(TxLocation::Mined) as i32, + block_hash: Some([11u8; 16].to_vec()), + confirmations: 2, + block_height: block_height_b, + }, + ]; let batch_query_response = TxQueryBatchResponsesProto { responses: transaction_query_batch_responses, @@ -3727,6 +3727,7 @@ fn test_coinbase_abandoned() { block_headers.insert(i, block_header.clone()); } alice_ts_interface.base_node_rpc_mock_state.set_blocks(block_headers); + runtime .block_on(alice_ts_interface.transaction_service_handle.validate_transactions()) .expect("Validation should start"); @@ -3799,6 +3800,7 @@ fn test_coinbase_abandoned() { block_headers.insert(i, block_header.clone()); } alice_ts_interface.base_node_rpc_mock_state.set_blocks(block_headers); + runtime .block_on(alice_ts_interface.transaction_service_handle.validate_transactions()) .expect("Validation should start"); @@ -3839,15 +3841,16 @@ fn test_coinbase_abandoned() { ); }); - let tx = runtime + let txs = runtime .block_on( alice_ts_interface .transaction_service_handle - .get_completed_transaction(tx_id2), + .get_cancelled_completed_transactions(), ) .unwrap(); - assert_eq!(tx.status, TransactionStatus::Coinbase); - assert!(!tx.valid); + + assert!(txs.get(&tx_id1).is_some()); + assert!(txs.get(&tx_id2).is_some()); let balance = runtime .block_on(alice_ts_interface.output_manager_service_handle.get_balance()) @@ -3903,7 +3906,7 @@ fn test_coinbase_abandoned() { .expect("Validation should start"); runtime.block_on(async { - let delay = sleep(Duration::from_secs(30)); + let delay = sleep(Duration::from_secs(60)); tokio::pin!(delay); let mut count = 0usize; loop { @@ -5277,12 +5280,11 @@ fn broadcast_all_completed_transactions_on_startup() { status: TransactionStatus::Completed, message: "Yo!".to_string(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Outbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, diff --git a/base_layer/wallet/tests/transaction_service_tests/storage.rs b/base_layer/wallet/tests/transaction_service_tests/storage.rs index 5b07e1fd83..145acf2f23 100644 --- a/base_layer/wallet/tests/transaction_service_tests/storage.rs +++ b/base_layer/wallet/tests/transaction_service_tests/storage.rs @@ -53,7 +53,13 @@ use tari_wallet::{ test_utils::create_consensus_constants, transaction_service::storage::{ database::{DbKeyValuePair, TransactionBackend, TransactionDatabase, WriteOperation}, - models::{CompletedTransaction, InboundTransaction, OutboundTransaction, WalletTransaction}, + models::{ + CompletedTransaction, + InboundTransaction, + OutboundTransaction, + TxCancellationReason, + WalletTransaction, + }, sqlite_db::TransactionServiceSqliteDatabase, }, }; @@ -266,12 +272,12 @@ pub fn test_db_backend(backend: T) { }, message: messages[i].clone(), timestamp: Utc::now().naive_utc(), - cancelled: false, + cancelled: None, direction: TransactionDirection::Outbound, coinbase_block_height: None, send_count: 0, last_send_timestamp: None, - valid: true, + transaction_signature: tx.first_kernel_excess_sig().unwrap_or(&Signature::default()).clone(), confirmations: None, mined_height: None, @@ -323,7 +329,7 @@ pub fn test_db_backend(backend: T) { assert!(runtime.block_on(db.fetch_last_mined_transaction()).unwrap().is_none()); runtime - .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, true, 10, [0u8; 16].to_vec(), 5, true, false)) + .block_on(db.set_transaction_mined_height(completed_txs[0].tx_id, 10, [0u8; 16].to_vec(), 5, true, false)) .unwrap(); assert_eq!( @@ -365,7 +371,7 @@ pub fn test_db_backend(backend: T) { .block_on(db.get_cancelled_completed_transaction(cancelled_tx_id)) .is_err()); runtime - .block_on(db.reject_completed_transaction(cancelled_tx_id)) + .block_on(db.reject_completed_transaction(cancelled_tx_id, TxCancellationReason::Unknown)) .unwrap(); let completed_txs_map = runtime.block_on(db.get_completed_transactions()).unwrap(); assert_eq!(completed_txs_map.len(), num_completed_txs - 1); diff --git a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs index c84666355f..5954f8d7db 100644 --- a/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs +++ b/base_layer/wallet/tests/transaction_service_tests/transaction_protocols.rs @@ -77,7 +77,7 @@ use tari_wallet::{ service::TransactionServiceResources, storage::{ database::TransactionDatabase, - models::CompletedTransaction, + models::{CompletedTransaction, TxCancellationReason}, sqlite_db::TransactionServiceSqliteDatabase, }, }, @@ -176,7 +176,6 @@ pub async fn setup() -> ( pub async fn add_transaction_to_database( tx_id: TxId, amount: MicroTari, - valid: bool, status: Option, coinbase_block_height: Option, db: TransactionDatabase, @@ -185,7 +184,7 @@ pub async fn add_transaction_to_database( let (_utxo, uo0) = make_input(&mut OsRng, 10 * amount, &factories.commitment); let (txs1, _uou1) = schema_to_transaction(&[txn_schema!(from: vec![uo0.clone()], to: vec![amount])]); let tx1 = (*txs1[0]).clone(); - let mut completed_tx1 = CompletedTransaction::new( + let completed_tx1 = CompletedTransaction::new( tx_id, CommsPublicKey::default(), CommsPublicKey::default(), @@ -199,7 +198,6 @@ pub async fn add_transaction_to_database( coinbase_block_height, None, ); - completed_tx1.valid = valid; db.insert_completed_transaction(tx_id, completed_tx1).await.unwrap(); } @@ -253,7 +251,7 @@ async fn tx_broadcast_protocol_submit_success() { // Fails because there is no transaction in the database to be broadcast assert!(join_handle.await.unwrap().is_err()); - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; let db_completed_tx = resources.db.get_completed_transaction(1.into()).await.unwrap(); assert!(db_completed_tx.confirmations.is_none()); @@ -322,7 +320,7 @@ async fn tx_broadcast_protocol_submit_rejection() { ) = setup().await; let mut event_stream = resources.event_publisher.subscribe(); - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; let timeout_update_watch = Watch::new(Duration::from_secs(1)); wallet_connectivity.notify_base_node_set(server_node_identity.to_peer()); // Now we add the connection @@ -392,7 +390,7 @@ async fn tx_broadcast_protocol_restart_protocol_as_query() { wallet_connectivity, ) = setup().await; - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; // Set Base Node query response to be not stored, as if the base node does not have the tx in its pool rpc_service_state.set_transaction_query_response(TxQueryResponse { @@ -480,7 +478,7 @@ async fn tx_broadcast_protocol_submit_success_followed_by_rejection() { ) = setup().await; let mut event_stream = resources.event_publisher.subscribe(); - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; resources.config.transaction_mempool_resubmission_window = Duration::from_secs(3); resources.config.broadcast_monitoring_timeout = Duration::from_secs(60); @@ -568,7 +566,7 @@ async fn tx_broadcast_protocol_submit_already_mined() { _transaction_event_receiver, wallet_connectivity, ) = setup().await; - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; // Set Base Node to respond with AlreadyMined rpc_service_state.set_submit_transaction_response(TxSubmissionResponse { @@ -633,7 +631,7 @@ async fn tx_broadcast_protocol_submit_and_base_node_gets_changed() { wallet_connectivity, ) = setup().await; - add_transaction_to_database(1.into(), 1 * T, true, None, None, resources.db.clone()).await; + add_transaction_to_database(1.into(), 1 * T, None, None, resources.db.clone()).await; resources.config.broadcast_monitoring_timeout = Duration::from_secs(60); @@ -737,7 +735,6 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { add_transaction_to_database( 1.into(), 1 * T, - true, Some(TransactionStatus::Broadcast), None, resources.db.clone(), @@ -746,7 +743,6 @@ async fn tx_validation_protocol_tx_becomes_mined_unconfirmed_then_confirmed() { add_transaction_to_database( 2.into(), 2 * T, - true, Some(TransactionStatus::Completed), None, resources.db.clone(), @@ -891,7 +887,6 @@ async fn tx_revalidation() { add_transaction_to_database( 1.into(), 1 * T, - true, Some(TransactionStatus::Completed), None, resources.db.clone(), @@ -900,7 +895,6 @@ async fn tx_revalidation() { add_transaction_to_database( 2.into(), 2 * T, - true, Some(TransactionStatus::Completed), None, resources.db.clone(), @@ -1026,7 +1020,6 @@ async fn tx_validation_protocol_reorg() { add_transaction_to_database( i.into(), i * T, - true, Some(TransactionStatus::Broadcast), None, resources.db.clone(), @@ -1037,16 +1030,15 @@ async fn tx_validation_protocol_reorg() { add_transaction_to_database( 6.into(), 6 * T, - true, Some(TransactionStatus::Coinbase), Some(8), resources.db.clone(), ) .await; + add_transaction_to_database( 7.into(), 7 * T, - true, Some(TransactionStatus::Coinbase), Some(9), resources.db.clone(), @@ -1280,10 +1272,14 @@ async fn tx_validation_protocol_reorg() { ); assert_eq!(completed_txs.get(&5.into()).cloned().unwrap().mined_height.unwrap(), 8); assert_eq!(completed_txs.get(&5.into()).cloned().unwrap().confirmations.unwrap(), 1); - - assert!(!completed_txs.get(&6.into()).unwrap().valid); assert_eq!( completed_txs.get(&7.into()).unwrap().status, TransactionStatus::Coinbase ); + let cancelled_completed_txs = resources.db.get_cancelled_completed_transactions().await.unwrap(); + + assert!(matches!( + cancelled_completed_txs.get(&6.into()).unwrap().cancelled, + Some(TxCancellationReason::AbandonedCoinbase) + )); } diff --git a/base_layer/wallet_ffi/src/callback_handler_tests.rs b/base_layer/wallet_ffi/src/callback_handler_tests.rs index 2e1271fbe0..1c5e39de39 100644 --- a/base_layer/wallet_ffi/src/callback_handler_tests.rs +++ b/base_layer/wallet_ffi/src/callback_handler_tests.rs @@ -53,10 +53,9 @@ mod test { test_utils::make_wallet_database_connection, transaction_service::{ handle::TransactionEvent, - protocols::TxRejection, storage::{ database::TransactionDatabase, - models::{CompletedTransaction, InboundTransaction, OutboundTransaction}, + models::{CompletedTransaction, InboundTransaction, OutboundTransaction, TxCancellationReason}, sqlite_db::TransactionServiceSqliteDatabase, }, }, @@ -309,7 +308,9 @@ mod test { runtime .block_on(db.insert_completed_transaction(5u64.into(), completed_tx_cancelled.clone())) .unwrap(); - runtime.block_on(db.reject_completed_transaction(5u64.into())).unwrap(); + runtime + .block_on(db.reject_completed_transaction(5u64.into(), TxCancellationReason::Unknown)) + .unwrap(); let faux_unconfirmed_tx = CompletedTransaction::new( 6u64.into(), @@ -511,7 +512,7 @@ mod test { transaction_event_sender .send(Arc::new(TransactionEvent::TransactionCancelled( 3u64.into(), - TxRejection::UserCancelled, + TxCancellationReason::UserCancelled, ))) .unwrap(); let start = Instant::now(); @@ -530,14 +531,14 @@ mod test { transaction_event_sender .send(Arc::new(TransactionEvent::TransactionCancelled( 4u64.into(), - TxRejection::UserCancelled, + TxCancellationReason::UserCancelled, ))) .unwrap(); transaction_event_sender .send(Arc::new(TransactionEvent::TransactionCancelled( 5u64.into(), - TxRejection::UserCancelled, + TxCancellationReason::UserCancelled, ))) .unwrap(); diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 1c3d1a92ae..8c0bdec891 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -2071,10 +2071,10 @@ pub unsafe extern "C" fn completed_transaction_get_message( result.into_raw() } -/// Check if a TariCompletedTransaction is Valid or not +/// This function checks to determine if a TariCompletedTransaction was originally a TariPendingOutboundTransaction /// /// ## Arguments -/// `transaction` - The pointer to a TariCompletedTransaction +/// `tx` - The TariCompletedTransaction /// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions /// as an out parameter. /// @@ -2084,23 +2084,27 @@ pub unsafe extern "C" fn completed_transaction_get_message( /// # Safety /// None #[no_mangle] -pub unsafe extern "C" fn completed_transaction_is_valid( - transaction: *mut TariCompletedTransaction, +pub unsafe extern "C" fn completed_transaction_is_outbound( + tx: *mut TariCompletedTransaction, error_out: *mut c_int, ) -> bool { let mut error = 0; ptr::swap(error_out, &mut error as *mut c_int); - if transaction.is_null() { - error = LibWalletError::from(InterfaceError::NullError("transaction".to_string())).code; + if tx.is_null() { + error = LibWalletError::from(InterfaceError::NullError("tx".to_string())).code; ptr::swap(error_out, &mut error as *mut c_int); return false; } - (*transaction).valid + if (*tx).direction == TransactionDirection::Outbound { + return true; + } + + false } -/// This function checks to determine if a TariCompletedTransaction was originally a TariPendingOutboundTransaction +/// Gets the number of confirmations of a TariCompletedTransaction /// /// ## Arguments /// `tx` - The TariCompletedTransaction @@ -2108,32 +2112,28 @@ pub unsafe extern "C" fn completed_transaction_is_valid( /// as an out parameter. /// /// ## Returns -/// `bool` - Returns if the transaction was originally sent from the wallet +/// `c_ulonglong` - Returns the number of confirmations of a Completed Transaction /// /// # Safety /// None #[no_mangle] -pub unsafe extern "C" fn completed_transaction_is_outbound( +pub unsafe extern "C" fn completed_transaction_get_confirmations( tx: *mut TariCompletedTransaction, error_out: *mut c_int, -) -> bool { +) -> c_ulonglong { let mut error = 0; ptr::swap(error_out, &mut error as *mut c_int); if tx.is_null() { error = LibWalletError::from(InterfaceError::NullError("tx".to_string())).code; ptr::swap(error_out, &mut error as *mut c_int); - return false; - } - - if (*tx).direction == TransactionDirection::Outbound { - return true; + return 0; } - false + (*tx).confirmations.unwrap_or(0) } -/// Gets the number of confirmations of a TariCompletedTransaction +/// Gets the reason a TariCompletedTransaction is cancelled, if it is indeed cancelled /// /// ## Arguments /// `tx` - The TariCompletedTransaction @@ -2141,15 +2141,25 @@ pub unsafe extern "C" fn completed_transaction_is_outbound( /// as an out parameter. /// /// ## Returns -/// `c_ulonglong` - Returns the number of confirmations of a Completed Transaction -/// +/// `c_int` - Returns the reason for cancellation which corresponds to: +/// | Value | Interpretation | +/// |---|---| +/// | -1 | Not Cancelled | +/// | 0 | Unknown | +/// | 1 | UserCancelled | +/// | 2 | Timeout | +/// | 3 | DoubleSpend | +/// | 4 | Orphan | +/// | 5 | TimeLocked | +/// | 6 | InvalidTransaction | +/// | 7 | AbandonedCoinbase | /// # Safety /// None #[no_mangle] -pub unsafe extern "C" fn completed_transaction_get_confirmations( +pub unsafe extern "C" fn completed_transaction_get_cancellation_reason( tx: *mut TariCompletedTransaction, error_out: *mut c_int, -) -> c_ulonglong { +) -> c_int { let mut error = 0; ptr::swap(error_out, &mut error as *mut c_int); @@ -2159,7 +2169,10 @@ pub unsafe extern "C" fn completed_transaction_get_confirmations( return 0; } - (*tx).confirmations.unwrap_or(0) + match (*tx).cancelled { + None => -1i32, + Some(reason) => reason as i32, + } } /// Frees memory for a TariCompletedTransaction @@ -3244,8 +3257,8 @@ unsafe fn init_logging( /// is whether if was successful or not. /// `callback_transaction_cancellation` - The callback function pointer matching /// the function signature. This is called when a transaction is cancelled. The first parameter is a pointer to the -/// cancelled transaction, the second is a reason as to why said transaction failed that is mapped to the `TxRejection` -/// enum: pub enum TxRejection { +/// cancelled transaction, the second is a reason as to why said transaction failed that is mapped to the +/// `TxCancellationReason` enum: pub enum TxCancellationReason { /// Unknown, // 0 /// UserCancelled, // 1 /// Timeout, // 2 diff --git a/base_layer/wallet_ffi/wallet.h b/base_layer/wallet_ffi/wallet.h index 0627b77452..a8b5d36d67 100644 --- a/base_layer/wallet_ffi/wallet.h +++ b/base_layer/wallet_ffi/wallet.h @@ -283,9 +283,6 @@ unsigned long long completed_transaction_get_transaction_id(struct TariCompleted // Gets the timestamp of a TariCompletedTransaction unsigned long long completed_transaction_get_timestamp(struct TariCompletedTransaction *transaction, int *error_out); -// Check if a TariCompletedTransaction is Valid or not -bool completed_transaction_is_valid(struct TariCompletedTransaction *tx, int *error_out); - // Checks if a TariCompletedTransaction was originally a TariPendingOutboundTransaction, // i.e the transaction was originally sent from the wallet bool completed_transaction_is_outbound(struct TariCompletedTransaction *tx, int *error_out); @@ -296,6 +293,30 @@ unsigned long long completed_transaction_get_confirmations(struct TariCompletedT // Gets the TariTransactionKernel of a TariCompletedTransaction struct TariTransactionKernel *completed_transaction_get_transaction_kernel(struct TariCompletedTransaction *transaction, int *error_out); +/// Gets the reason a TariCompletedTransaction is cancelled, if it is indeed cancelled +/// +/// ## Arguments +/// `tx` - The TariCompletedTransaction +/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions +/// as an out parameter. +/// +/// ## Returns +/// `c_int` - Returns the reason for cancellation which corresponds to: +/// | Value | Interpretation | +/// |--- |--- | +/// | -1 | Not Cancelled | +/// | 0 | Unknown | +/// | 1 | UserCancelled | +/// | 2 | Timeout | +/// | 3 | DoubleSpend | +/// | 4 | Orphan | +/// | 5 | TimeLocked | +/// | 6 | InvalidTransaction | +/// | 7 | AbandonedCoinbase | +/// # Safety +/// None +int completed_transaction_get_cancellation_reason(struct TariCompletedTransaction *transaction, int *error_out); + // Frees memory for a TariCompletedTransaction void completed_transaction_destroy(struct TariCompletedTransaction *transaction); @@ -475,8 +496,8 @@ struct TariPublicKeys *comms_list_connected_public_keys(struct TariWallet *walle /// when a direct send is completed. The first parameter is the transaction id and the second is whether if was successful or not. /// `callback_transaction_cancellation` - The callback function pointer matching the function signature. This is called /// when a transaction is cancelled. The first parameter is a pointer to the cancelled transaction, the second is a reason as to -/// why said transaction failed that is mapped to the `TxRejection` enum: -/// pub enum TxRejection { +/// why said transaction failed that is mapped to the `TxCancellationReason` enum: +/// pub enum TxCancellationReason { /// Unknown, // 0 /// UserCancelled, // 1 /// Timeout, // 2 diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index 6ab1d20cdf..902da40406 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -45,8 +45,8 @@ Feature: Wallet CLI And I wait 30 seconds And I stop wallet SENDER And I send 1000000 uT from SENDER to RECEIVER via command line - Then wallet SENDER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and valid - Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and valid + Then wallet SENDER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled And mining node MINE mines 5 blocks Then I wait for wallet RECEIVER to have at least 1000000 uT @@ -64,7 +64,7 @@ Feature: Wallet CLI And I wait 30 seconds And I stop wallet SENDER And I send one-sided 1000000 uT from SENDER to RECEIVER via command line - Then wallet SENDER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and valid + Then wallet SENDER has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled And mining node MINE mines 5 blocks Then I wait for wallet RECEIVER to have at least 1000000 uT @@ -82,8 +82,8 @@ Feature: Wallet CLI And I wait 30 seconds And I stop wallet SENDER And I make it rain from wallet SENDER 1 tx per sec 10 sec 8000 uT 100 increment to RECEIVER via command line - Then wallet SENDER has at least 10 transactions that are all TRANSACTION_STATUS_BROADCAST and valid - Then wallet RECEIVER has at least 10 transactions that are all TRANSACTION_STATUS_BROADCAST and valid + Then wallet SENDER has at least 10 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled + Then wallet RECEIVER has at least 10 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled And mining node MINE mines 5 blocks Then I wait for wallet RECEIVER to have at least 84500 uT @@ -100,9 +100,9 @@ Feature: Wallet CLI And I wait 30 seconds And I stop wallet WALLET And I do coin split on wallet WALLET to 10000 uT 10 coins via command line - Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and valid + Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_BROADCAST and not cancelled And mining node MINE mines 5 blocks - Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and valid + Then wallet WALLET has at least 1 transactions that are all TRANSACTION_STATUS_MINED_CONFIRMED and not cancelled And I stop wallet WALLET Then I get count of utxos of wallet WALLET and it's at least 10 via command line diff --git a/integration_tests/features/WalletFFI.feature b/integration_tests/features/WalletFFI.feature index a00b643873..27fce2ac0b 100644 --- a/integration_tests/features/WalletFFI.feature +++ b/integration_tests/features/WalletFFI.feature @@ -154,10 +154,10 @@ Feature: Wallet FFI Then ffi wallet FFI_WALLET detects AT_LEAST 2 ffi transactions to be TRANSACTION_STATUS_BROADCAST And mining node MINER mines 2 blocks Then all nodes are at height 22 - Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and valid + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_UNCONFIRMED and not cancelled And mining node MINER mines 5 blocks Then all nodes are at height 27 - Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and valid + Then wallet RECEIVER has at least 1 transactions that are all TRANSACTION_STATUS_FAUX_CONFIRMED and not cancelled And I stop ffi wallet FFI_WALLET @critical diff --git a/integration_tests/features/WalletMonitoring.feature b/integration_tests/features/WalletMonitoring.feature index b22e3eab0a..89c0b09e5f 100644 --- a/integration_tests/features/WalletMonitoring.feature +++ b/integration_tests/features/WalletMonitoring.feature @@ -46,7 +46,7 @@ Feature: Wallet Monitoring When I wait 30 seconds And I list all COINBASE transactions for wallet WALLET_A1 And I list all COINBASE transactions for wallet WALLET_B1 - Then all COINBASE transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing validity + Then all COINBASE transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing cancellation # 18+ mins on circle ci @long-running @@ -108,11 +108,11 @@ Feature: Wallet Monitoring And I list all NORMAL transactions for wallet WALLET_A1 And I list all NORMAL transactions for wallet WALLET_B1 # TODO: Uncomment this step when wallets can handle reorg -# Then all NORMAL transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing validity +# Then all NORMAL transactions for wallet WALLET_A1 and wallet WALLET_B1 have consistent but opposing cancellation And I list all NORMAL transactions for wallet WALLET_A2 And I list all NORMAL transactions for wallet WALLET_B2 # TODO: Uncomment this step when wallets can handle reorg -# Then all NORMAL transactions for wallet WALLET_A2 and wallet WALLET_B2 have consistent but opposing validity +# Then all NORMAL transactions for wallet WALLET_A2 and wallet WALLET_B2 have consistent but opposing cancellation When I wait 1 seconds Scenario Outline: Verify all coinbases in hybrid mining are accounted for diff --git a/integration_tests/features/support/wallet_steps.js b/integration_tests/features/support/wallet_steps.js index 16e80af104..5dd9f68a6e 100644 --- a/integration_tests/features/support/wallet_steps.js +++ b/integration_tests/features/support/wallet_steps.js @@ -1993,7 +1993,7 @@ Then( ); Then( - /wallet (.*) has at least (.*) transactions that are all (.*) and valid/, + /wallet (.*) has at least (.*) transactions that are all (.*) and not cancelled/, { timeout: 610 * 1000 }, async function (walletName, numberOfTransactions, transactionStatus) { const walletClient = await this.getWallet(walletName).connectClient(); @@ -2003,7 +2003,7 @@ Then( numberOfTransactions + " transactions to be " + transactionStatus + - " and valid..." + " and not cancelled..." ); var transactions; var numberCorrect; @@ -2026,7 +2026,7 @@ Then( for (let i = 0; i < transactions.length; i++) { if ( transactions[i]["status"] !== transactionStatus || - !transactions[i]["valid"] + transactions[i]["is_cancelled"] ) { console.log( "Transaction " + @@ -2036,8 +2036,8 @@ Then( transactions[i]["status"] + " (need " + transactionStatus + - ") and is valid(" + - transactions[i]["valid"] + + ") and is not cancelled(" + + transactions[i]["is_cancelled"] + ")" ); statusCorrect = false; @@ -2060,7 +2060,7 @@ Then( ); Then( - /all (.*) transactions for wallet (.*) and wallet (.*) have consistent but opposing validity/, + /all (.*) transactions for wallet (.*) and wallet (.*) have consistent but opposing cancellation status/, async function (transaction_type, walletNameA, walletNameB) { let walletClientA = await this.getWallet(walletNameA).connectClient(); let walletClientB = await this.getWallet(walletNameB).connectClient(); @@ -2079,31 +2079,31 @@ Then( if (transactionsA === undefined || transactionsB === undefined) { expect("\nNo `" + type + "` transactions found!").to.equal(""); } - let validA = transactionsA[0]["valid"]; + let cancelledA = transactionsA[0]["is_cancelled"]; for (let i = 0; i < transactionsA.length; i++) { - if (validA !== transactionsA[i]["valid"]) { + if (cancelledA !== transactionsA[i]["is_cancelled"]) { expect( "\n" + walletNameA + "'s `" + type + - "` transactions do not have a consistent validity status" + "` transactions do not have a consistent cancellation status" ).to.equal(""); } } - let validB = transactionsB[0]["valid"]; + let cancelledB = transactionsB[0]["is_cancelled"]; for (let i = 0; i < transactionsB.length; i++) { - if (validB !== transactionsB[i]["valid"]) { + if (cancelledB !== transactionsB[i]["is_cancelled"]) { expect( "\n" + walletNameB + "'s `" + type + - "` transactions do not have a consistent validity status" + "` transactions do not have a consistent cancellation status" ).to.equal(""); } } - expect(validA).to.equal(!validB); + expect(cancelledA).to.equal(!cancelledB); } ); @@ -2124,7 +2124,7 @@ Then( expect("\nNo `" + type + "` transactions found!").to.equal(""); } for (let i = 0; i < transactions.length; i++) { - expect(transactions[i]["valid"]).to.equal(true); + expect(transactions[i]["is_cancelled"]).to.equal(false); } } ); diff --git a/integration_tests/helpers/ffi/completedTransaction.js b/integration_tests/helpers/ffi/completedTransaction.js index 8d0defdeb3..70362f6657 100644 --- a/integration_tests/helpers/ffi/completedTransaction.js +++ b/integration_tests/helpers/ffi/completedTransaction.js @@ -62,10 +62,6 @@ class CompletedTransaction { return InterfaceFFI.completedTransactionGetTimestamp(this.ptr); } - isValid() { - return InterfaceFFI.completedTransactionIsValid(this.ptr); - } - getConfirmations() { return InterfaceFFI.completedTransactionGetConfirmations(this.ptr); } diff --git a/integration_tests/helpers/ffi/ffiInterface.js b/integration_tests/helpers/ffi/ffiInterface.js index cb98d17bf4..f1480e2128 100644 --- a/integration_tests/helpers/ffi/ffiInterface.js +++ b/integration_tests/helpers/ffi/ffiInterface.js @@ -152,7 +152,6 @@ class InterfaceFFI { this.ulonglong, [this.ptr, this.intPtr], ], - completed_transaction_is_valid: [this.bool, [this.ptr, this.intPtr]], completed_transaction_is_outbound: [this.bool, [this.ptr, this.intPtr]], completed_transaction_get_confirmations: [ this.ulonglong, @@ -163,6 +162,10 @@ class InterfaceFFI { this.ptr, [this.ptr, this.intPtr], ], + completed_transaction_get_cancellation_reason: [ + this.ptr, + [this.ptr, this.intPtr], + ], completed_transactions_get_length: [this.uint, [this.ptr, this.intPtr]], completed_transactions_get_at: [ this.ptr, @@ -854,17 +857,10 @@ class InterfaceFFI { return result; } - static completedTransactionIsValid(ptr) { - let error = this.initError(); - let result = this.fn.completed_transaction_is_valid(ptr, error); - this.checkErrorResult(error, `completedTransactionIsValid`); - return result; - } - static completedTransactionIsOutbound(ptr) { let error = this.initError(); let result = this.fn.completed_transaction_is_outbound(ptr, error); - this.checkErrorResult(error, `completedTransactionGetConfirmations`); + this.checkErrorResult(error, `completedTransactionGetIsOutbound`); return result; } @@ -881,7 +877,17 @@ class InterfaceFFI { ptr, error ); - this.checkErrorResult(error, `completedTransactionGetConfirmations`); + this.checkErrorResult(error, `completedTransactionGetKernel`); + return result; + } + + static completedTransactionGetCancellationReason(ptr) { + let error = this.initError(); + let result = this.fn.completed_transaction_get_cancellation_reason( + ptr, + error + ); + this.checkErrorResult(error, `completedTransactionGetCancellationReason`); return result; } diff --git a/integration_tests/helpers/util.js b/integration_tests/helpers/util.js index 411dd8072f..d042b30add 100644 --- a/integration_tests/helpers/util.js +++ b/integration_tests/helpers/util.js @@ -298,9 +298,7 @@ function consoleLogTransactionDetails(txnDetails) { " has status " + pad("'" + txnDetails.status + "'", 40) + " and " + - pad("is_cancelled(" + txnDetails.is_cancelled + ")", 21) + - " and " + - pad("is_valid(" + txnDetails.valid + ")", 16) + pad("is_cancelled(" + txnDetails.is_cancelled + ")", 21) ); } diff --git a/integration_tests/helpers/walletClient.js b/integration_tests/helpers/walletClient.js index 030e2ae253..df69471130 100644 --- a/integration_tests/helpers/walletClient.js +++ b/integration_tests/helpers/walletClient.js @@ -57,7 +57,6 @@ class WalletClient { Number(data[i].transaction.timestamp.seconds) * 1000 ), message: data[i].transaction.message, - valid: data[i].transaction.valid, }); } return transactions; @@ -81,10 +80,7 @@ class WalletClient { const data = await this.getAllCoinbaseTransactions(); const transactions = []; for (let i = 0; i < data.length; i++) { - if ( - transactionStatus().indexOf(data[i].status) === 6 && - data[i].valid === true - ) { + if (transactionStatus().indexOf(data[i].status) === 6) { transactions.push(data[i]); } } @@ -109,10 +105,7 @@ class WalletClient { const data = await this.getAllCoinbaseTransactions(); let count = 0; for (let i = 0; i < data.length; i++) { - if ( - transactionStatus().indexOf(data[i].status) == 6 && - data[i].valid == true - ) { + if (transactionStatus().indexOf(data[i].status) == 6) { count += 1; } } @@ -229,10 +222,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) >= 2 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) >= 2) { return true; } else { return false; @@ -248,10 +238,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) == 2 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) == 2) { return true; } else { return false; @@ -267,10 +254,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) >= 3 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) >= 3) { return true; } else { return false; @@ -286,10 +270,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) >= 4 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) >= 4) { return true; } else { return false; @@ -305,10 +286,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) >= 5 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) >= 5) { return true; } else { return false; @@ -324,10 +302,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) == 5 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) == 5) { return true; } else { return false; @@ -343,10 +318,7 @@ class WalletClient { const txnDetails = await this.getTransactionInfo({ transaction_ids: [tx_id.toString()], }); - if ( - transactionStatus().indexOf(txnDetails.transactions[0].status) == 6 && - txnDetails.transactions[0].valid - ) { + if (transactionStatus().indexOf(txnDetails.transactions[0].status) == 6) { return true; } else { return false; diff --git a/integration_tests/helpers/walletFFIClient.js b/integration_tests/helpers/walletFFIClient.js index bc1b230d0c..9f3ef79329 100644 --- a/integration_tests/helpers/walletFFIClient.js +++ b/integration_tests/helpers/walletFFIClient.js @@ -180,6 +180,10 @@ class WalletFFIClient { return this.wallet.getOutboundTransactions(); } + getCancelledTransactions() { + return this.wallet.walletGetCancelledTransactions(); + } + cancelPendingTransaction(tx_id) { return this.wallet.cancelPendingTransaction(tx_id); } From 8f54705ad0f7fa5a1a9aade32d969bd1c6a896b0 Mon Sep 17 00:00:00 2001 From: Byron Hambly Date: Thu, 17 Feb 2022 09:05:04 +0200 Subject: [PATCH 23/28] ci: cargo test speedups (#3843) Description --- Reduces CI test time to approx 30 mins on cache hit, 40ish mins on cache miss - caching: use [Swatinem/rust-cache](/Swatinem/rust-cache) for smarter caching - test: split test into compile and test run, hopefully gets rid of our `SIGILL` problems - remove test dependency on check nightly step since the cache was missed anyway - run tests in debug as it builds faster With thanks to https://matklad.github.io/2021/09/04/fast-rust-builds.html for the ideas --- .github/workflows/ci.yml | 90 ++++++++++++---------------------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56d5a2bd43..ce9f7f6626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,17 +24,13 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 - - name: caching - uses: actions/cache@v2 + - name: toolchain + uses: actions-rs/toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-clippy-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-clippy- - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}- + toolchain: ${{ env.toolchain }} + components: clippy, rustfmt + override: true + - uses: Swatinem/rust-cache@v1 - name: ubuntu dependencies run: | sudo apt-get update && \ @@ -63,12 +59,6 @@ jobs: cd applications/launchpad/gui-vue npm ci npm run build - - name: toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.toolchain }} - components: clippy, rustfmt - override: true - name: cargo fmt uses: actions-rs/cargo@v1 with: @@ -85,17 +75,13 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 - - name: caching - uses: actions/cache@v2 + - name: toolchain + uses: actions-rs/toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-build-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-build- - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}- + toolchain: ${{ env.toolchain }} + profile: minimal + override: true + - uses: Swatinem/rust-cache@v1 - name: ubuntu dependencies run: | sudo apt-get update && \ @@ -116,12 +102,6 @@ jobs: cd applications/launchpad/gui-vue npm ci npm run build - - name: toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.toolchain }} - components: clippy, rustfmt - override: true - name: cargo check uses: actions-rs/cargo@v1 with: @@ -133,17 +113,13 @@ jobs: steps: - name: checkout uses: actions/checkout@v2 - - name: caching - uses: actions/cache@v2 + - name: toolchain + uses: actions-rs/toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-${{ runner.cpu-model }}-stable-build-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.cpu-model }}-stable-build- - ${{ runner.os }}-${{ runner.cpu-model }}-stable- + toolchain: stable + profile: minimal + override: true + - uses: Swatinem/rust-cache@v1 - name: ubuntu dependencies run: | sudo apt-get update && \ @@ -164,12 +140,6 @@ jobs: cd applications/launchpad/gui-vue npm ci npm run build - - name: toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true - name: rustup show run: | rustup show @@ -180,22 +150,16 @@ jobs: args: --release --all-targets test: name: test - needs: build + # needs: build runs-on: ubuntu-18.04 steps: - name: checkout uses: actions/checkout@v2 - - name: caching - uses: actions/cache@v2 + - name: toolchain + uses: actions-rs/toolchain@v1 with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-test-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}-test- - ${{ runner.os }}-${{ runner.cpu-model }}-${{ env.toolchain }}- + toolchain: ${{ env.toolchain }} + - uses: Swatinem/rust-cache@v1 - name: ubuntu dependencies run: | sudo apt-get update && \ @@ -216,12 +180,12 @@ jobs: cd applications/launchpad/gui-vue npm ci npm run build - - name: toolchain - uses: actions-rs/toolchain@v1 + - name: cargo test compile + uses: actions-rs/cargo@v1 with: - toolchain: ${{ env.toolchain }} + command: test + args: --no-run --locked - name: cargo test uses: actions-rs/cargo@v1 with: command: test - args: --release --all-targets From 7c0a4b1bb7d675229414ba36e49377f78aae9c65 Mon Sep 17 00:00:00 2001 From: "C.Lee Taylor" <47312074+leet4tari@users.noreply.github.com> Date: Thu, 17 Feb 2022 09:07:02 +0200 Subject: [PATCH 24/28] ci: improvements to macos pkg (#3824) Copy OSX signed bins into pkg, update step labels, improve cache hits, add disk space feedback How Has This Been Tested? OSX pkg changes have not been tested, but all other parts have been test in the local fork. --- .github/workflows/base_node_binaries.yml | 44 +++++++++++++++++++----- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.github/workflows/base_node_binaries.yml b/.github/workflows/base_node_binaries.yml index aec488f199..d7f7ae4127 100644 --- a/.github/workflows/base_node_binaries.yml +++ b/.github/workflows/base_node_binaries.yml @@ -20,7 +20,7 @@ env: jobs: builds: - name: Build and deploy tari_base_node + name: Build and upload Binaries strategy: fail-fast: false matrix: @@ -94,6 +94,7 @@ jobs: libappindicator3-dev \ patchelf \ librsvg2-dev + - name: Install macOS dependencies if: startsWith(runner.os,'macOS') run: brew install cmake zip coreutils automake autoconf @@ -141,6 +142,9 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-${{ matrix.os }}-${{ matrix.target_cpu }}-${{ matrix.features }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.os }}-${{ matrix.target_cpu }}-${{ matrix.features }}-cargo-build-target- + ${{ runner.os }}-${{ matrix.os }}-${{ matrix.target_cpu }}-${{ matrix.features }}- - name: Compile launchpad GUI run: | @@ -154,6 +158,12 @@ jobs: npm install npm run build + - name: Info - Pre-Compile Space Check for Nix + if: "!startsWith(runner.os,'Windows')" + shell: bash + run: | + df -h + - name: Build rust binaries env: RUSTFLAGS: "-C target_cpu=${{ matrix.target_cpu }}" @@ -162,6 +172,12 @@ jobs: echo "Cache Key: ${{ runner.os }}-${{ matrix.os }}-${{ matrix.target_cpu }}-${{ matrix.features }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}" cargo build --release + - name: Info - Post-Compile Space Check for Nix + if: "!startsWith(runner.os,'Windows')" + shell: bash + run: | + df -h + - name: Copy binaries to folder for zipping shell: bash run: | @@ -185,8 +201,9 @@ jobs: cp -v "$GITHUB_WORKSPACE/target/release/tari_launchpad${TBN_EXT}" . cp -v "$GITHUB_WORKSPACE/applications/tari_base_node/${PLATFORM_SPECIFIC_DIR}/runtime/start_tor${SHELL_EXT}" . - - name: Build the macos pkg + - name: Build the macOS pkg if: startsWith(runner.os,'macOS') + continue-on-error: true env: MACOS_KEYCHAIN_PASS: ${{ secrets.MACOS_KEYCHAIN_PASS }} MACOS_APPLICATION_ID: ${{ secrets.MACOS_APPLICATION_ID }} @@ -206,10 +223,19 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $MACOS_KEYCHAIN_PASS build.keychain cd buildtools ./create_osx_install_zip.sh unused nozip - FILES=("tari_base_node" "tari_console_wallet" "tari_mining_node" "tari_merge_mining_proxy") + FILES=( + "tari_base_node" + "tari_console_wallet" + "tari_mining_node" + "tari_merge_mining_proxy" + "tari_validator_node" + "tari_collectibles" + "tari_launchpad" + ) for FILE in "${FILES[@]}"; do codesign --force -s "Developer ID Application: $MACOS_APPLICATION_ID" "/tmp/tari_testnet/runtime/$FILE" -v codesign --verify --deep --display --verbose=4 "/tmp/tari_testnet/runtime/$FILE" + cp -vf "/tmp/tari_testnet/runtime/$FILE" "$GITHUB_WORKSPACE${{ env.TBN_DIST }}" done pkgbuild --root /tmp/tari_testnet \ --identifier "com.tarilabs.pkg" \ @@ -218,15 +244,17 @@ jobs: --scripts "/tmp/tari_testnet/scripts" \ --sign "Developer ID Installer: $MACOS_INSTALLER_ID" \ "${{ github.workspace }}${{ env.TBN_DIST }}/tari-${{ env.VERSION }}.pkg" - - name: Artifact macos pkg + + - name: Artifact upload for macOS pkg if: startsWith(runner.os,'macOS') + continue-on-error: true uses: actions/upload-artifact@v2 with: name: tari-${{ env.VERSION }}.pkg path: "${{ github.workspace }}${{ env.TBN_DIST }}/tari-${{ env.VERSION }}.pkg" # unlike inno script studio, iscc.exe doesn't run the [precompile] step generate_config.bat - - name: Build the windows installer + - name: Build the Windows installer shell: cmd if: startsWith(runner.os,'Windows') run: | @@ -234,7 +262,7 @@ jobs: call generate_config.bat "%programfiles(x86)%\Inno Setup 6\iscc.exe" "/DMyAppVersion=${{ env.VERSION }}-${{ env.VSHA_SHORT }}-release" "windows_inno_installer.iss" - - name: Upload artifact for Windows installer + - name: Artifact upload for Windows installer uses: actions/upload-artifact@v2 if: startsWith(runner.os,'Windows') with: @@ -255,14 +283,14 @@ jobs: ${SHARUN} --check "${{ env.BINFILE }}.zip.sha256" #rm -f "${BINFILE}" - - name: Artifact archive + - name: Artifact upload for Archive uses: actions/upload-artifact@v2 with: #name: ${{ env.TBN_FILENAME }}-${{ env.VERSION }}-${{ env.VSHA_SHORT }}-${{ matrix.os }}-${{ matrix.target_cpu }}-${{ matrix.features }} name: tari_binary_archive-${{ matrix.os }}-${{ matrix.target_cpu }} path: "${{ github.workspace }}${{ env.TBN_DIST }}/${{ env.BINFILE }}.zip*" - - name: Artifact miner + - name: Artifact upload for Miner uses: actions/upload-artifact@v2 with: name: tari_mining_node-${{ matrix.os }}-${{ matrix.target_cpu }} From 815ba8ea39fdfde97f3a79dabc4e74bf76ea5363 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Thu, 17 Feb 2022 10:57:39 +0100 Subject: [PATCH 25/28] fix: daily test (#3815) Description --- Run `git reset --hard` before `git pull` to ensure the pull succeed. How Has This Been Tested? --- `node cron_jobs.js` --- applications/daily_tests/cron_jobs.js | 23 ++++++++++++++++++----- applications/daily_tests/helpers.js | 6 ++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/applications/daily_tests/cron_jobs.js b/applications/daily_tests/cron_jobs.js index d3041c6250..17fd67aa9b 100644 --- a/applications/daily_tests/cron_jobs.js +++ b/applications/daily_tests/cron_jobs.js @@ -123,6 +123,10 @@ ${logLines.join("\n")} async function main() { console.log("👩‍💻 Updating repo..."); + await git.reset(__dirname).catch((err) => { + console.error("🚨 Failed to do git reset --hard"); + console.error(err); + }); await git.pull(__dirname).catch((err) => { console.error("🚨 Failed to update git repo"); console.error(err); @@ -136,11 +140,20 @@ async function main() { ).start(); new CronJob("30 1 * * *", () => runBaseNodeSyncTest(SyncType.Pruned)).start(); new CronJob("0 0 * * *", () => - git.pull(__dirname).catch((err) => { - failed("Failed to update git repo"); - console.error(err); - return Promise.resolve(null); - }) + git + .reset(__dirname) + .catch((err) => { + console.error("🚨 Failed to do git reset --hard"); + console.error(err); + return Promise.resolve(null); + }) + .then( + git.pull(__dirname).catch((err) => { + failed("Failed to update git repo"); + console.error(err); + return Promise.resolve(null); + }) + ) ).start(); console.log("⏱ Cron jobs started."); diff --git a/applications/daily_tests/helpers.js b/applications/daily_tests/helpers.js index 1dd97a016f..e01c6608fd 100644 --- a/applications/daily_tests/helpers.js +++ b/applications/daily_tests/helpers.js @@ -154,10 +154,10 @@ async function monitorProcessOutput({ } const git = { - pull(cwd = null) { + command(params, cwd = null) { cwd = cwd || process.cwd(); return new Promise((resolve, reject) => { - let ps = spawn("git", ["pull", "--rebase"], { cwd }); + let ps = spawn("git", params, { cwd }); ps.stdout.on("data", (buf) => { console.log(buf.toString()); }); @@ -173,6 +173,8 @@ const git = { }); }); }, + reset: (cwd = null) => git.command(["reset", "--hard"], cwd), + pull: (cwd = null) => git.command(["pull", "--rebase"], cwd), }; module.exports = { From e67eaa50372a58a8c257de6378cd3b96e1795446 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Thu, 17 Feb 2022 10:58:13 +0100 Subject: [PATCH 26/28] chore: add icons to applications (#3812) Description --- Add icons for `base node`, `console wallet` and `merge mining proxy` binaries (Windows). How Has This Been Tested? --- Visually --- applications/tari_base_node/build.rs | 11 +++++++++++ applications/tari_base_node/icon.ico | Bin 0 -> 285478 bytes applications/tari_base_node/icon.rc | 1 + applications/tari_base_node/icon.res | Bin 0 -> 285684 bytes applications/tari_console_wallet/build.rs | 11 +++++++++++ applications/tari_console_wallet/icon.ico | Bin 0 -> 285478 bytes applications/tari_console_wallet/icon.rc | 1 + applications/tari_console_wallet/icon.res | Bin 0 -> 285684 bytes applications/tari_merge_mining_proxy/build.rs | 11 +++++++++++ applications/tari_merge_mining_proxy/icon.ico | Bin 0 -> 285478 bytes applications/tari_merge_mining_proxy/icon.rc | 1 + applications/tari_merge_mining_proxy/icon.res | Bin 0 -> 285684 bytes 12 files changed, 36 insertions(+) create mode 100644 applications/tari_base_node/build.rs create mode 100644 applications/tari_base_node/icon.ico create mode 100644 applications/tari_base_node/icon.rc create mode 100644 applications/tari_base_node/icon.res create mode 100644 applications/tari_console_wallet/build.rs create mode 100644 applications/tari_console_wallet/icon.ico create mode 100644 applications/tari_console_wallet/icon.rc create mode 100644 applications/tari_console_wallet/icon.res create mode 100644 applications/tari_merge_mining_proxy/build.rs create mode 100644 applications/tari_merge_mining_proxy/icon.ico create mode 100644 applications/tari_merge_mining_proxy/icon.rc create mode 100644 applications/tari_merge_mining_proxy/icon.res diff --git a/applications/tari_base_node/build.rs b/applications/tari_base_node/build.rs new file mode 100644 index 0000000000..1c502c938a --- /dev/null +++ b/applications/tari_base_node/build.rs @@ -0,0 +1,11 @@ +#[cfg(windows)] +fn main() { + use std::env; + println!("cargo:rerun-if-changed=icon.res"); + let mut path = env::current_dir().unwrap(); + path.push("icon.res"); + println!("cargo:rustc-link-arg={}", path.into_os_string().into_string().unwrap()); +} + +#[cfg(not(windows))] +fn main() {} diff --git a/applications/tari_base_node/icon.ico b/applications/tari_base_node/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..974b0eaf7bd83154c6c5f293180efbbf3d2fe620 GIT binary patch literal 285478 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 literal 0 HcmV?d00001 diff --git a/applications/tari_merge_mining_proxy/build.rs b/applications/tari_merge_mining_proxy/build.rs new file mode 100644 index 0000000000..1c502c938a --- /dev/null +++ b/applications/tari_merge_mining_proxy/build.rs @@ -0,0 +1,11 @@ +#[cfg(windows)] +fn main() { + use std::env; + println!("cargo:rerun-if-changed=icon.res"); + let mut path = env::current_dir().unwrap(); + path.push("icon.res"); + println!("cargo:rustc-link-arg={}", path.into_os_string().into_string().unwrap()); +} + +#[cfg(not(windows))] +fn main() {} diff --git a/applications/tari_merge_mining_proxy/icon.ico b/applications/tari_merge_mining_proxy/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e952cc0f247df37ac75d4d2d2bb7d5be69e369ce GIT binary patch literal 285478 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 Z0{2(o{tDb*f%_|Pe+BNZz_0xk_`mQtnBM>Z literal 0 HcmV?d00001 diff --git a/applications/tari_merge_mining_proxy/icon.rc b/applications/tari_merge_mining_proxy/icon.rc new file mode 100644 index 0000000000..26527deb57 --- /dev/null +++ b/applications/tari_merge_mining_proxy/icon.rc @@ -0,0 +1 @@ +mining_node ICON "icon.ico" \ No newline at end of file diff --git a/applications/tari_merge_mining_proxy/icon.res b/applications/tari_merge_mining_proxy/icon.res new file mode 100644 index 0000000000000000000000000000000000000000..722ccfba07597ab1f74d4dc4e9755bba1ea1bd7c GIT binary patch literal 285684 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
  1. hLVkey-Qh-zey_z*>mx0<4JuE^r@1Sa-p-7=wH! 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

    gLsHARQa-R0a{`MT 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%0D=U3=G6sOeBrYnvV>l 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 literal 0 HcmV?d00001 From 8df9021bc83a870cb8bb1671181cde8c051bd7f6 Mon Sep 17 00:00:00 2001 From: Martin Stefcek <35243812+Cifko@users.noreply.github.com> Date: Thu, 17 Feb 2022 10:58:44 +0100 Subject: [PATCH 27/28] chore: honor decimals in ERC20 (#3809) Description --- You can now send floating points of erc20 coins. e.g. 1.07 How Has This Been Tested? --- Manually. --- .../web-app/src/AccountDashboard.js | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/applications/tari_collectibles/web-app/src/AccountDashboard.js b/applications/tari_collectibles/web-app/src/AccountDashboard.js index 0b79e1d1b1..aa25391e8f 100644 --- a/applications/tari_collectibles/web-app/src/AccountDashboard.js +++ b/applications/tari_collectibles/web-app/src/AccountDashboard.js @@ -176,14 +176,23 @@ class AccountDashboard extends React.Component { }; onSendToAmountChanged = async (e) => { - this.setState({ sendToAmount: parseInt(e.target.value) }); + if ( + RegExp(`^\\d*(\\.\\d{0,${this.state.tip002Data.decimals}})?$`).test( + e.target.value + ) + ) + this.setState({ sendToAmount: e.target.value }); }; onSend = async () => { try { this.setState({ error: "" }); + let sendToAmount = Math.round( + Number(this.state.sendToAmount) * + Math.pow(10, this.state.tip002Data.decimals) + ); let result = await binding.command_asset_wallets_send_to( this.state.assetPublicKey, - this.state.sendToAmount, + sendToAmount, this.state.sendToAddress ); console.log(result); @@ -242,7 +251,7 @@ class AccountDashboard extends React.Component { )} - {this.state.assetInfo.name}{" "} + {this.state.assetInfo.name} {this.state.hasAssetWallet ? ( ) : ( @@ -271,9 +280,9 @@ class AccountDashboard extends React.Component { TIP002 - Balance:{" "} + Balance: {this.state.balance / - Math.pow(10, this.state.tip002Data.decimals)}{" "} + Math.pow(10, this.state.tip002Data.decimals)} {this.state.tip002Data.symbol} @@ -286,7 +295,7 @@ class AccountDashboard extends React.Component { @@ -365,6 +374,6 @@ AccountDashboard.propTypes = { assetPubKey: PropTypes.string, }), }).isRequired, -} +}; export default withRouter(AccountDashboard); From 86e01542e6b92794613bfdb32ca54d28c5e19ed7 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 17 Feb 2022 13:59:27 +0400 Subject: [PATCH 28/28] fix(wallet): fix aggressive disconnects in wallet connectivity (#3807) Description --- - Disconnects the wallet peer only if the pool is unable to create a new RPC session. - Clear the `pool` when disconnecting, so that requests for a client session will wait - Immediately return an error if dialling a peer that is banned Motivation and Context --- Observed the wallet continuously disconnecting and reconnecting to base node, resulting in many of these message: `Outbound messaging protocol failed for peer xxxxx: IO Error: 5e5731a0/4: connection is closed` This is not a confirmed fix for the issue, because it is difficult to reproduce, but I suspect the problem was not clearing the pool in wallet connectivity when disconnecting was causing disconnect to be called whenever a peer session was requested by the monitor etc., sometimes timed in such a way that it would disconnect the new connection that was just established, and that would continue indefinitely. How Has This Been Tested? --- Manually, switching base nodes, stop base node while connected then restart --- .../src/connectivity_service/service.rs | 52 +++++++++++++------ comms/src/connectivity/manager.rs | 15 ++++++ comms/src/peer_manager/manager.rs | 4 ++ comms/src/peer_manager/peer_storage.rs | 7 +++ 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/base_layer/wallet/src/connectivity_service/service.rs b/base_layer/wallet/src/connectivity_service/service.rs index df57075270..4122f9acea 100644 --- a/base_layer/wallet/src/connectivity_service/service.rs +++ b/base_layer/wallet/src/connectivity_service/service.rs @@ -24,7 +24,7 @@ use std::{mem, time::Duration}; use log::*; use tari_comms::{ - connectivity::ConnectivityRequester, + connectivity::{ConnectivityError, ConnectivityRequester}, peer_manager::{NodeId, Peer}, protocol::rpc::{RpcClientLease, RpcClientPool}, PeerConnection, @@ -116,12 +116,21 @@ impl WalletConnectivityService { } async fn check_connection(&mut self) { - if let Some(pool) = self.pools.as_ref() { - if !pool.base_node_wallet_rpc_client.is_connected().await { - debug!(target: LOG_TARGET, "Peer connection lost. Attempting to reconnect..."); + debug!(target: LOG_TARGET, "HERE1"); + match self.pools.as_ref() { + Some(pool) => { + debug!(target: LOG_TARGET, "HERE2"); + if !pool.base_node_wallet_rpc_client.is_connected().await { + debug!(target: LOG_TARGET, "Peer connection lost. Attempting to reconnect..."); + self.set_online_status(OnlineStatus::Offline); + self.setup_base_node_connection().await; + } + }, + None => { + debug!(target: LOG_TARGET, "No connection. Attempting to connect..."); self.set_online_status(OnlineStatus::Offline); self.setup_base_node_connection().await; - } + }, } } @@ -216,10 +225,12 @@ impl WalletConnectivityService { } async fn disconnect_base_node(&mut self, node_id: NodeId) { - if let Ok(Some(connection)) = self.connectivity.get_connection(node_id.clone()).await { - if connection.clone().disconnect().await.is_ok() { - debug!(target: LOG_TARGET, "Disconnected base node peer {}", node_id); + if let Ok(Some(mut connection)) = self.connectivity.get_connection(node_id.clone()).await { + match connection.disconnect().await { + Ok(_) => debug!(target: LOG_TARGET, "Disconnected base node peer {}", node_id), + Err(e) => error!(target: LOG_TARGET, "Failed to disconnect base node: {}", e), } + self.pools = None; }; } @@ -248,20 +259,29 @@ impl WalletConnectivityService { break; }, Ok(false) => { - // Retry with updated peer - self.disconnect_base_node(node_id).await; + debug!( + target: LOG_TARGET, + "The peer has changed while connecting. Attempting to connect to new base node." + ); + continue; + }, + Err(WalletConnectivityError::ConnectivityError(ConnectivityError::DialCancelled)) => { + debug!( + target: LOG_TARGET, + "Dial was cancelled. Retrying after {}s ...", + self.config.base_node_monitor_refresh_interval.as_secs() + ); + self.set_online_status(OnlineStatus::Offline); time::sleep(self.config.base_node_monitor_refresh_interval).await; continue; }, Err(e) => { - if self.current_base_node() != Some(node_id.clone()) { - self.set_online_status(OnlineStatus::Connecting); - } else { + warn!(target: LOG_TARGET, "{}", e); + if self.current_base_node().as_ref() == Some(&node_id) { + self.disconnect_base_node(node_id).await; self.set_online_status(OnlineStatus::Offline); + time::sleep(self.config.base_node_monitor_refresh_interval).await; } - warn!(target: LOG_TARGET, "{}", e); - self.disconnect_base_node(node_id).await; - time::sleep(self.config.base_node_monitor_refresh_interval).await; continue; }, } diff --git a/comms/src/connectivity/manager.rs b/comms/src/connectivity/manager.rs index b0ae56ea5b..2872ff58cd 100644 --- a/comms/src/connectivity/manager.rs +++ b/comms/src/connectivity/manager.rs @@ -228,6 +228,21 @@ impl ConnectivityManagerActor { let span = span!(Level::TRACE, "handle_request"); span.follows_from(tracing_id); async move { + match self.peer_manager.is_peer_banned(&node_id).await { + Ok(true) => { + if let Some(reply) = reply_tx { + let _ = reply.send(Err(ConnectionManagerError::PeerBanned)); + } + return; + }, + Ok(false) => {}, + Err(err) => { + if let Some(reply) = reply_tx { + let _ = reply.send(Err(err.into())); + } + return; + }, + } match self.pool.get(&node_id) { Some(state) if state.is_connected() => { debug!( diff --git a/comms/src/peer_manager/manager.rs b/comms/src/peer_manager/manager.rs index e10b0be988..6dc1a3d5c5 100644 --- a/comms/src/peer_manager/manager.rs +++ b/comms/src/peer_manager/manager.rs @@ -264,6 +264,10 @@ impl PeerManager { .ban_peer_by_node_id(node_id, duration, reason) } + pub async fn is_peer_banned(&self, node_id: &NodeId) -> Result { + self.peer_storage.read().await.is_peer_banned(node_id) + } + /// Changes the offline flag bit of the peer. Return the previous offline state. pub async fn set_offline(&self, node_id: &NodeId, is_offline: bool) -> Result { self.peer_storage.write().await.set_offline(node_id, is_offline) diff --git a/comms/src/peer_manager/peer_storage.rs b/comms/src/peer_manager/peer_storage.rs index 0f9ca71ac3..75e6413705 100644 --- a/comms/src/peer_manager/peer_storage.rs +++ b/comms/src/peer_manager/peer_storage.rs @@ -498,6 +498,13 @@ where DS: KeyValueStore Ok(node_id) } + pub fn is_peer_banned(&self, node_id: &NodeId) -> Result { + let peer = self + .find_by_node_id(node_id)? + .ok_or(PeerManagerError::PeerNotFoundError)?; + Ok(peer.is_banned()) + } + /// Changes the OFFLINE flag bit of the peer. pub fn set_offline(&mut self, node_id: &NodeId, offline: bool) -> Result { let peer_key = *self