diff --git a/Cargo.lock b/Cargo.lock index d0b97edc11..f2c610889d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1130,6 +1130,7 @@ dependencies = [ "const-hex", "evmlib", "eyre", + "hex 0.4.3", "libp2p 0.54.1", "rand 0.8.5", "rmp-serde", @@ -1151,6 +1152,22 @@ dependencies = [ "xor_name", ] +[[package]] +name = "autonomi_cli" +version = "0.1.0" +dependencies = [ + "autonomi", + "clap", + "color-eyre", + "dirs-next", + "indicatif", + "sn_build_info", + "sn_logging", + "sn_peers_acquisition", + "tokio", + "tracing", +] + [[package]] name = "axum" version = "0.6.20" diff --git a/Cargo.toml b/Cargo.toml index 79cc1a5945..c34946d706 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "autonomi", + "autonomi_cli", "evmlib", "evm_testnet", # "sn_auditor", diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index aeeebe5aaf..876936e617 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -27,6 +27,7 @@ curv = { version = "0.10.1", package = "sn_curv", default-features = false, feat eip2333 = { version = "0.2.1", package = "sn_bls_ckd" } const-hex = "1.12.0" evmlib = { path = "../evmlib", version = "0.1" } +hex = "~0.4.3" libp2p = "0.54.1" rand = "0.8.5" rmp-serde = "1.1.1" diff --git a/autonomi/src/client/address.rs b/autonomi/src/client/address.rs new file mode 100644 index 0000000000..e390c62d0a --- /dev/null +++ b/autonomi/src/client/address.rs @@ -0,0 +1,42 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use xor_name::XorName; + +#[derive(Debug, thiserror::Error)] +pub enum DataError { + #[error("Invalid XorName")] + InvalidXorName, + #[error("Input address is not a hex string")] + InvalidHexString, +} + +pub fn str_to_xorname(addr: &str) -> Result { + let bytes = hex::decode(addr).map_err(|_| DataError::InvalidHexString)?; + let xor = XorName(bytes.try_into().map_err(|_| DataError::InvalidXorName)?); + Ok(xor) +} + +pub fn xorname_to_str(addr: XorName) -> String { + hex::encode(addr) +} + +#[cfg(test)] +mod test { + use super::*; + use xor_name::XorName; + + #[test] + fn test_xorname_to_str() { + let rng = &mut rand::thread_rng(); + let xorname = XorName::random(rng); + let str = xorname_to_str(xorname); + let xorname2 = str_to_xorname(&str).expect("Failed to convert back to xorname"); + assert_eq!(xorname, xorname2); + } +} \ No newline at end of file diff --git a/autonomi/src/client/data.rs b/autonomi/src/client/data.rs index c1cd0ecdd7..8681e6a47f 100644 --- a/autonomi/src/client/data.rs +++ b/autonomi/src/client/data.rs @@ -13,7 +13,7 @@ use evmlib::common::{QuoteHash, QuotePayment, TxHash}; use evmlib::wallet::Wallet; use libp2p::futures; use rand::{thread_rng, Rng}; -use sn_evm::ProofOfPayment; +use sn_evm::{Amount, AttoTokens, ProofOfPayment}; use sn_networking::PutRecordCfg; use sn_networking::{GetRecordCfg, Network, NetworkError, PayeeQuote, VerificationKind}; use sn_protocol::{ @@ -54,7 +54,9 @@ pub enum PayError { #[error("Could not simultaneously fetch store costs: {0:?}")] JoinError(JoinError), #[error("Wallet error: {0:?}")] - WalletError(#[from] wallet::Error), + EvmWalletError(#[from] wallet::Error), + #[error("Failed to self-encrypt data.")] + SelfEncryption(#[from] crate::self_encryption::Error), } /// Errors that can occur during the get operation. @@ -184,6 +186,27 @@ impl Client { Ok(map_xor_name) } + pub(crate) async fn cost( + &mut self, + data: Bytes, + ) -> Result { + let now = std::time::Instant::now(); + let (data_map_chunk, chunks) = encrypt(data)?; + + tracing::debug!("Encryption took: {:.2?}", now.elapsed()); + + let map_xor_name = *data_map_chunk.address().xorname(); + let mut content_addrs = vec![map_xor_name]; + + for chunk in &chunks { + content_addrs.push(*chunk.name()); + } + + let cost_map = self.get_store_quotes(content_addrs.into_iter()).await?; + let total_cost = AttoTokens::from_atto(cost_map.iter().map(|(_, quote)| quote.2.cost.as_atto()).sum::()); + Ok(total_cost) + } + pub(crate) async fn pay( &mut self, content_addrs: impl Iterator, diff --git a/autonomi/src/client/files.rs b/autonomi/src/client/files.rs index 524fc6fb7e..84735408e4 100644 --- a/autonomi/src/client/files.rs +++ b/autonomi/src/client/files.rs @@ -1,8 +1,9 @@ use crate::client::data::{GetError, PutError}; use crate::client::Client; +use crate::self_encryption::encrypt; use bytes::Bytes; -use evmlib::wallet::Wallet; use serde::{Deserialize, Serialize}; +use sn_evm::{Amount, AttoTokens}; use std::collections::HashMap; use std::path::PathBuf; use walkdir::WalkDir; @@ -61,12 +62,59 @@ impl Client { Ok(data) } + /// Get the cost to upload a file/dir to the network. + /// quick and dirty implementation, please refactor once files are cleanly implemented + pub async fn file_cost( + &mut self, + path: &PathBuf, + ) -> Result { + let mut map = HashMap::new(); + let mut total_cost = Amount::ZERO; + + for entry in WalkDir::new(path) { + let entry = entry?; + + if !entry.file_type().is_file() { + continue; + } + + let path = entry.path().to_path_buf(); + tracing::info!("Cost for file: {path:?}"); + + let data = tokio::fs::read(&path).await?; + let file_bytes = Bytes::from(data); + let file_cost = self.cost(file_bytes.clone()).await.expect("TODO"); + + total_cost += file_cost.as_atto(); + + // re-do encryption to get the correct map xorname here + // this code needs refactor + let now = std::time::Instant::now(); + let (data_map_chunk, _) = encrypt(file_bytes).expect("TODO"); + tracing::debug!("Encryption took: {:.2?}", now.elapsed()); + let map_xor_name = *data_map_chunk.address().xorname(); + let data_map_xorname = FilePointer { + data_map: map_xor_name, + created_at: 0, + modified_at: 0, + }; + + map.insert(path, data_map_xorname); + } + + let root = Root { map }; + let root_serialized = rmp_serde::to_vec(&root).expect("TODO"); + + let cost = self.cost(Bytes::from(root_serialized)).await.expect("TODO"); + Ok(cost) + } + /// Upload a directory to the network. The directory is recursively walked. #[cfg(feature = "fs")] pub async fn upload_from_dir( &mut self, path: PathBuf, - wallet: &Wallet, + wallet: &sn_evm::EvmWallet, ) -> Result<(Root, XorName), UploadError> { let mut map = HashMap::new(); @@ -79,6 +127,7 @@ impl Client { let path = entry.path().to_path_buf(); tracing::info!("Uploading file: {path:?}"); + println!("Uploading file: {path:?}"); let file = upload_from_file(self, path.clone(), wallet).await?; map.insert(path, file); @@ -96,7 +145,7 @@ impl Client { async fn upload_from_file( client: &mut Client, path: PathBuf, - wallet: &Wallet, + wallet: &sn_evm::EvmWallet, ) -> Result { let data = tokio::fs::read(path).await?; let data = Bytes::from(data); diff --git a/autonomi/src/client/mod.rs b/autonomi/src/client/mod.rs index 2900ae12b7..bef8e7db34 100644 --- a/autonomi/src/client/mod.rs +++ b/autonomi/src/client/mod.rs @@ -1,3 +1,5 @@ +pub mod address; + #[cfg(feature = "data")] pub mod data; #[cfg(feature = "files")] diff --git a/autonomi/src/lib.rs b/autonomi/src/lib.rs index 0e28f17dcb..ca78b2d20a 100644 --- a/autonomi/src/lib.rs +++ b/autonomi/src/lib.rs @@ -25,6 +25,9 @@ pub mod client; #[cfg(feature = "data")] mod self_encryption; +pub use sn_evm::EvmWallet as Wallet; +pub use sn_evm::EvmNetwork as Network; + #[doc(no_inline)] // Place this under 'Re-exports' in the docs. pub use bytes::Bytes; #[doc(no_inline)] // Place this under 'Re-exports' in the docs. diff --git a/autonomi/tests/evm/file.rs b/autonomi/tests/evm/file.rs new file mode 100644 index 0000000000..746cd1cea3 --- /dev/null +++ b/autonomi/tests/evm/file.rs @@ -0,0 +1,82 @@ +#[cfg(feature = "evm-payments")] +mod test { + + use crate::common; + use crate::common::{evm_network_from_env, evm_wallet_from_env_or_default}; + use autonomi::Client; + use bytes::Bytes; + use eyre::bail; + use std::time::Duration; + use tokio::time::sleep; + + #[tokio::test] + async fn file() -> Result<(), Box> { + common::enable_logging(); + + let network = evm_network_from_env(); + let mut client = Client::connect(&[]).await.unwrap(); + let mut wallet = evm_wallet_from_env_or_default(network); + + // let data = common::gen_random_data(1024 * 1024 * 1000); + // let user_key = common::gen_random_data(32); + + let (root, addr) = client + .upload_from_dir("tests/file/test_dir".into(), &mut wallet) + .await?; + + sleep(Duration::from_secs(10)).await; + + let root_fetched = client.fetch_root(addr).await?; + + assert_eq!( + root.map, root_fetched.map, + "root fetched should match root put" + ); + + Ok(()) + } + + #[cfg(feature = "vault")] + #[tokio::test] + async fn file_into_vault() -> eyre::Result<()> { + common::enable_logging(); + + let network = evm_network_from_env(); + + let mut client = Client::connect(&[]) + .await? + .with_vault_entropy(Bytes::from("at least 32 bytes of entropy here"))?; + + let mut wallet = evm_wallet_from_env_or_default(network); + + let (root, addr) = client + .upload_from_dir("tests/file/test_dir".into(), &mut wallet) + .await?; + sleep(Duration::from_secs(2)).await; + + let root_fetched = client.fetch_root(addr).await?; + + assert_eq!( + root.map, root_fetched.map, + "root fetched should match root put" + ); + + // now assert over the stored account packet + let new_client = Client::connect(&[]) + .await? + .with_vault_entropy(Bytes::from("at least 32 bytes of entropy here"))?; + + if let Some(ap) = new_client.fetch_and_decrypt_vault().await? { + let ap_root_fetched = Client::deserialise_root(ap)?; + + assert_eq!( + root.map, ap_root_fetched.map, + "root fetched should match root put" + ); + } else { + bail!("No account packet found"); + } + + Ok(()) + } +} diff --git a/autonomi_cli/Cargo.toml b/autonomi_cli/Cargo.toml new file mode 100644 index 0000000000..a1c6bd5705 --- /dev/null +++ b/autonomi_cli/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "autonomi_cli" +version = "0.1.0" +edition = "2021" + +[features] +default = ["metrics"] +local-discovery = ["sn_peers_acquisition/local-discovery"] +metrics = ["sn_logging/process-metrics"] +network-contacts = ["sn_peers_acquisition/network-contacts"] + +[dependencies] +autonomi = { path = "../autonomi", version = "0.1.0", features = ["data", "files"] } +clap = { version = "4.2.1", features = ["derive"] } +color-eyre = "~0.6" +dirs-next = "~2.0.0" +indicatif = { version = "0.17.5", features = ["tokio"] } +tokio = { version = "1.32.0", features = [ + "io-util", + "macros", + "parking_lot", + "rt", + "sync", + "time", + "fs", +] } +tracing = { version = "~0.1.26" } +sn_peers_acquisition = { path = "../sn_peers_acquisition", version = "0.5.0" } +sn_build_info = { path = "../sn_build_info", version = "0.1.11" } +sn_logging = { path = "../sn_logging", version = "0.2.33" } + +[lints] +workspace = true diff --git a/autonomi_cli/README.md b/autonomi_cli/README.md new file mode 100644 index 0000000000..b10d2128fb --- /dev/null +++ b/autonomi_cli/README.md @@ -0,0 +1,27 @@ +# A CLI for the Autonomi Network + +``` +Usage: autonomi_cli [OPTIONS] + +Commands: + file Operations related to file handling + register Operations related to register management + vault Operations related to vault management + help Print this message or the help of the given subcommand(s) + +Options: + --log-output-dest + Specify the logging output destination. [default: data-dir] + --log-format + Specify the logging format. + --peer + Peer(s) to use for bootstrap, in a 'multiaddr' format containing the peer ID [env: SAFE_PEERS=] + --timeout + The maximum duration to wait for a connection to the network before timing out + -x, --no-verify + Prevent verification of data storage on the network + -h, --help + Print help (see more with '--help') + -V, --version + Print version +``` \ No newline at end of file diff --git a/autonomi_cli/src/actions/connect.rs b/autonomi_cli/src/actions/connect.rs new file mode 100644 index 0000000000..ee54c01586 --- /dev/null +++ b/autonomi_cli/src/actions/connect.rs @@ -0,0 +1,35 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::Client; +use autonomi::Multiaddr; +use color_eyre::eyre::bail; +use indicatif::ProgressBar; +use color_eyre::eyre::Result; +use std::time::Duration; + +pub async fn connect_to_network(peers: Vec) -> Result { + let progress_bar = ProgressBar::new_spinner(); + progress_bar.enable_steady_tick(Duration::from_millis(120)); + progress_bar.set_message("Connecting to The Autonomi Network..."); + let new_style = progress_bar.style().tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈🔗"); + progress_bar.set_style(new_style); + + progress_bar.set_message("Connecting to The Autonomi Network..."); + + match Client::connect(&peers).await { + Ok(client) => { + progress_bar.finish_with_message("Connected to the Network"); + Ok(client) + } + Err(e) => { + progress_bar.finish_with_message("Failed to connect to the network"); + bail!("Failed to connect to the network: {e}") + } + } +} diff --git a/autonomi_cli/src/actions/download.rs b/autonomi_cli/src/actions/download.rs new file mode 100644 index 0000000000..f27c3f3e37 --- /dev/null +++ b/autonomi_cli/src/actions/download.rs @@ -0,0 +1,51 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::{client::address::str_to_xorname, Client}; +use color_eyre::eyre::{eyre, Context, Result}; +use std::path::PathBuf; +use super::get_progress_bar; + +pub async fn download(addr: &str, dest_path: &str, client: &mut Client) -> Result<()> { + let address = str_to_xorname(addr) + .wrap_err("Failed to parse data address")?; + let root = client.fetch_root(address).await + .wrap_err("Failed to fetch data from address")?; + + let progress_bar = get_progress_bar(root.map.len() as u64)?; + let mut all_errs = vec![]; + for (path, file) in root.map { + progress_bar.println(format!("Fetching file: {path:?}...")); + let bytes = match client.fetch_file(&file).await { + Ok(bytes) => bytes, + Err(e) => { + let err = format!("Failed to fetch file {path:?}: {e}"); + all_errs.push(err); + continue; + } + }; + + let path = PathBuf::from(dest_path).join(path); + let here = PathBuf::from("."); + let parent = path.parent().unwrap_or_else(|| &here); + std::fs::create_dir_all(parent)?; + std::fs::write(path, bytes)?; + progress_bar.clone().inc(1); + } + progress_bar.finish_and_clear(); + + if all_errs.is_empty() { + println!("Successfully downloaded data at: {addr}"); + Ok(()) + } else { + let err_no = all_errs.len(); + eprintln!("{err_no} errors while downloading data at: {addr}"); + eprintln!("{all_errs:#?}"); + Err(eyre!("Errors while downloading data")) + } +} \ No newline at end of file diff --git a/autonomi_cli/src/actions/mod.rs b/autonomi_cli/src/actions/mod.rs new file mode 100644 index 0000000000..8b4662c3d9 --- /dev/null +++ b/autonomi_cli/src/actions/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +mod connect; +mod download; +mod progress_bar; + +pub use connect::connect_to_network; +pub use download::download; + +pub use progress_bar::get_progress_bar; diff --git a/autonomi_cli/src/actions/progress_bar.rs b/autonomi_cli/src/actions/progress_bar.rs new file mode 100644 index 0000000000..4c6bbdf7bf --- /dev/null +++ b/autonomi_cli/src/actions/progress_bar.rs @@ -0,0 +1,22 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use indicatif::{ProgressBar, ProgressStyle}; +use std::time::Duration; +use color_eyre::eyre::Result; + +pub fn get_progress_bar(length: u64) -> Result { + let progress_bar = ProgressBar::new(length); + progress_bar.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len}")? + .progress_chars("#>-"), + ); + progress_bar.enable_steady_tick(Duration::from_millis(100)); + Ok(progress_bar) +} diff --git a/autonomi_cli/src/commands.rs b/autonomi_cli/src/commands.rs new file mode 100644 index 0000000000..37fbebb36b --- /dev/null +++ b/autonomi_cli/src/commands.rs @@ -0,0 +1,135 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +mod file; +mod register; +mod vault; + +use clap::Subcommand; +use color_eyre::Result; + +use crate::opt::Opt; + +#[derive(Subcommand, Debug)] +pub enum SubCmd { + /// Operations related to file handling. + File { + #[command(subcommand)] + command: FileCmd, + }, + + /// Operations related to register management. + Register { + #[command(subcommand)] + command: RegisterCmd, + }, + + /// Operations related to vault management. + Vault { + #[command(subcommand)] + command: VaultCmd, + }, +} + +#[derive(Subcommand, Debug)] +pub enum FileCmd { + /// Estimate cost to upload a file. + Cost { + /// The file to estimate cost for. + file: String, + }, + + /// Upload a file and pay for it. + Upload { + /// The file to upload. + file: String, + }, + + /// Download a file from the given address. + Download { + /// The address of the file to download. + addr: String, + /// The destination file path. + dest_file: String, + }, + + /// List previous uploads + List, +} + +#[derive(Subcommand, Debug)] +pub enum RegisterCmd { + /// Estimate cost to register a name. + Cost { + /// The name to register. + name: String, + }, + + /// Create a new register with the given name and value. + Create { + /// The name of the register. + name: String, + /// The value to store in the register. + value: String, + }, + + /// Edit an existing register. + Edit { + /// The name of the register. + name: String, + /// The new value to store in the register. + value: String, + }, + + /// Get the value of a register. + Get { + /// The name of the register. + name: String, + }, + + /// List previous registers + List, +} + +#[derive(Subcommand, Debug)] +pub enum VaultCmd { + /// Estimate cost to create a vault. + Cost, + + /// Create a vault at a deterministic address based on your `SECRET_KEY`. + Create, + + /// Sync vault with the network, including registers and files. + Sync, +} + +pub async fn handle_subcommand(opt: Opt) -> Result<()> { + let peers = crate::utils::get_peers(opt.peers).await?; + let cmd = opt.command; + + match cmd { + SubCmd::File { command } => match command { + FileCmd::Cost { file } => file::cost(&file, peers).await, + FileCmd::Upload { file } => file::upload(&file, peers).await, + FileCmd::Download { addr, dest_file } => file::download(&addr, &dest_file, peers).await, + FileCmd::List => file::list(peers), + }, + SubCmd::Register { command } => match command { + RegisterCmd::Cost { name } => register::cost(&name, peers).await, + RegisterCmd::Create { name, value } => register::create(&name, &value, peers).await, + RegisterCmd::Edit { name, value } => register::edit(&name, &value, peers).await, + RegisterCmd::Get { name } => register::get(&name, peers).await, + RegisterCmd::List => register::list(peers), + }, + SubCmd::Vault { command } => match command { + VaultCmd::Cost => vault::cost(peers), + VaultCmd::Create => vault::create(peers), + VaultCmd::Sync => vault::sync(peers), + }, + } +} diff --git a/autonomi_cli/src/commands/file.rs b/autonomi_cli/src/commands/file.rs new file mode 100644 index 0000000000..acfbfc94f8 --- /dev/null +++ b/autonomi_cli/src/commands/file.rs @@ -0,0 +1,56 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::client::address::xorname_to_str; +use autonomi::Wallet; +use autonomi::Multiaddr; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; +use std::path::PathBuf; + +pub async fn cost(file: &str, peers: Vec) -> Result<()> { + let mut client = crate::actions::connect_to_network(peers).await?; + + println!("Getting upload cost..."); + let cost = client.file_cost(&PathBuf::from(file)).await + .wrap_err("Failed to calculate cost for file")?; + + println!("Estimate cost to upload file: {file}"); + println!("Total cost: {cost}"); + Ok(()) +} + +pub async fn upload(file: &str, peers: Vec) -> Result<()> { + let secret_key = crate::utils::get_secret_key() + .wrap_err("The secret key is required to perform this action")?; + let network = crate::utils::get_evm_network() + .wrap_err("Failed to get evm network")?; + let wallet = Wallet::new_from_private_key(network, &secret_key) + .wrap_err("Failed to load wallet")?; + + let mut client = crate::actions::connect_to_network(peers).await?; + + println!("Uploading data to network..."); + let (_, xor_name) = client.upload_from_dir(PathBuf::from(file), &wallet).await + .wrap_err("Failed to upload file")?; + let addr = xorname_to_str(xor_name); + + println!("Successfully uploaded: {file}"); + println!("At address: {addr}"); + Ok(()) +} + +pub async fn download(addr: &str, dest_path: &str, peers: Vec) -> Result<()> { + let mut client = crate::actions::connect_to_network(peers).await?; + crate::actions::download(addr, dest_path, &mut client).await +} + +pub fn list(_peers: Vec) -> Result<()> { + println!("Listing previous uploads..."); + Ok(()) +} diff --git a/autonomi_cli/src/commands/register.rs b/autonomi_cli/src/commands/register.rs new file mode 100644 index 0000000000..fd57d678e0 --- /dev/null +++ b/autonomi_cli/src/commands/register.rs @@ -0,0 +1,48 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::Multiaddr; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; + +pub async fn cost(name: &str, _peers: Vec) -> Result<()> { + let register_key = crate::utils::get_register_signing_key() + .wrap_err("The register key is required to perform this action")?; + println!("Estimate cost to register name: {name} with register key: {register_key}"); + Ok(()) +} + +pub async fn create(name: &str, value: &str, _peers: Vec) -> Result<()> { + let secret_key = crate::utils::get_secret_key() + .wrap_err("The secret key is required to perform this action")?; + let register_key = crate::utils::get_register_signing_key() + .wrap_err("The register key is required to perform this action")?; + println!( + "Creating register: {name} with value: {value} using secret key: {secret_key} and register key: {register_key}" + ); + Ok(()) +} + +pub async fn edit(name: &str, value: &str, _peers: Vec) -> Result<()> { + let register_key = crate::utils::get_register_signing_key() + .wrap_err("The register key is required to perform this action")?; + println!("Editing register: {name} with value: {value} using register key: {register_key}"); + Ok(()) +} + +pub async fn get(name: &str, _peers: Vec) -> Result<()> { + let register_key = crate::utils::get_register_signing_key() + .wrap_err("The register key is required to perform this action")?; + println!("Getting value of register: {name} with register key: {register_key}"); + Ok(()) +} + +pub fn list(_peers: Vec) -> Result<()> { + println!("Listing previous registers..."); + Ok(()) +} diff --git a/autonomi_cli/src/commands/vault.rs b/autonomi_cli/src/commands/vault.rs new file mode 100644 index 0000000000..9a8d708824 --- /dev/null +++ b/autonomi_cli/src/commands/vault.rs @@ -0,0 +1,25 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::Multiaddr; +use color_eyre::eyre::Result; + +pub fn cost(_peers: Vec) -> Result<()> { + println!("The vault feature is coming soon!"); + Ok(()) +} + +pub fn create(_peers: Vec) -> Result<()> { + println!("The vault feature is coming soon!"); + Ok(()) +} + +pub fn sync(_peers: Vec) -> Result<()> { + println!("The vault feature is coming soon!"); + Ok(()) +} diff --git a/autonomi_cli/src/log_metrics.rs b/autonomi_cli/src/log_metrics.rs new file mode 100644 index 0000000000..cc109f603f --- /dev/null +++ b/autonomi_cli/src/log_metrics.rs @@ -0,0 +1,39 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use color_eyre::Result; +#[cfg(feature = "metrics")] +use sn_logging::{metrics::init_metrics, Level, LogBuilder, LogFormat}; + +use crate::opt::Opt; + +pub fn init_logging_and_metrics(opt: &Opt) -> Result<()> { + let logging_targets = vec![ + ("sn_networking".to_string(), Level::INFO), + ("sn_build_info".to_string(), Level::TRACE), + ("autonomi".to_string(), Level::TRACE), + ("sn_logging".to_string(), Level::TRACE), + ("sn_peers_acquisition".to_string(), Level::TRACE), + ("sn_protocol".to_string(), Level::TRACE), + ("sn_registers".to_string(), Level::TRACE), + ("sn_evm".to_string(), Level::TRACE), + ]; + let mut log_builder = LogBuilder::new(logging_targets); + log_builder.output_dest(opt.log_output_dest.clone()); + log_builder.format(opt.log_format.unwrap_or(LogFormat::Default)); + let _log_handles = log_builder.initialize()?; + + #[cfg(feature = "metrics")] + std::thread::spawn(|| { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime to spawn metrics thread"); + rt.spawn(async { + init_metrics(std::process::id()).await; + }); + }); + Ok(()) +} diff --git a/autonomi_cli/src/main.rs b/autonomi_cli/src/main.rs new file mode 100644 index 0000000000..f07aaf4cc4 --- /dev/null +++ b/autonomi_cli/src/main.rs @@ -0,0 +1,36 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +#[macro_use] +extern crate tracing; + +mod commands; +mod log_metrics; +mod opt; +mod utils; +mod actions; + +use clap::Parser; +use color_eyre::Result; + +use opt::Opt; + +fn main() -> Result<()> { + color_eyre::install().expect("Failed to initialise error handler"); + let opt = Opt::parse(); + log_metrics::init_logging_and_metrics(&opt).expect("Failed to initialise logging and metrics"); + + // Log the full command that was run and the git version + info!("\"{}\"", std::env::args().collect::>().join(" ")); + let version = sn_build_info::git_info(); + info!("autonomi client built with git version: {version}"); + println!("autonomi client built with git version: {version}"); + + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + rt.block_on(commands::handle_subcommand(opt)) +} diff --git a/autonomi_cli/src/opt.rs b/autonomi_cli/src/opt.rs new file mode 100644 index 0000000000..8f3fb20967 --- /dev/null +++ b/autonomi_cli/src/opt.rs @@ -0,0 +1,61 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use std::time::Duration; + +use clap::Parser; +use color_eyre::Result; +use sn_logging::{LogFormat, LogOutputDest}; +use sn_peers_acquisition::PeersArgs; + +use crate::commands::SubCmd; + +// Please do not remove the blank lines in these doc comments. +// They are used for inserting line breaks when the help menu is rendered in the UI. +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub(crate) struct Opt { + /// Specify the logging output destination. + /// + /// Valid values are "stdout", "data-dir", or a custom path. + /// + /// `data-dir` is the default value. + /// + /// The data directory location is platform specific: + /// - Linux: $HOME/.local/share/safe/client/logs + /// - macOS: $HOME/Library/Application Support/safe/client/logs + /// - Windows: C:\Users\\AppData\Roaming\safe\client\logs + #[allow(rustdoc::invalid_html_tags)] + #[clap(long, value_parser = LogOutputDest::parse_from_str, verbatim_doc_comment, default_value = "data-dir")] + pub log_output_dest: LogOutputDest, + + /// Specify the logging format. + /// + /// Valid values are "default" or "json". + /// + /// If the argument is not used, the default format will be applied. + #[clap(long, value_parser = LogFormat::parse_from_str, verbatim_doc_comment)] + pub log_format: Option, + + #[command(flatten)] + pub(crate) peers: PeersArgs, + + /// Available sub commands. + #[clap(subcommand)] + pub command: SubCmd, + + /// The maximum duration to wait for a connection to the network before timing out. + #[clap(long = "timeout", global = true, value_parser = |t: &str| -> Result { Ok(t.parse().map(Duration::from_secs)?) })] + pub connection_timeout: Option, + + /// Prevent verification of data storage on the network. + /// + /// This may increase operation speed, but offers no guarantees that operations were successful. + #[clap(global = true, long = "no-verify", short = 'x')] + pub no_verify: bool, +} diff --git a/autonomi_cli/src/utils.rs b/autonomi_cli/src/utils.rs new file mode 100644 index 0000000000..71c8b779a7 --- /dev/null +++ b/autonomi_cli/src/utils.rs @@ -0,0 +1,91 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use autonomi::Multiaddr; +use autonomi::Network; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::Result; +use color_eyre::Section; +use sn_peers_acquisition::PeersArgs; +use std::env; +use std::fs; +use std::path::PathBuf; + +use sn_peers_acquisition::SAFE_PEERS_ENV; + +// NB TODO: use those as return values for the functions below +// use autonomi::register::RegisterKey; +// use autonomi::wallet::WalletKey; + +const SECRET_KEY: &str = "SECRET_KEY"; +const REGISTER_SIGNING_KEY: &str = "REGISTER_SIGNING_KEY"; + +const SECRET_KEY_FILE: &str = "secret_key"; +const REGISTER_SIGNING_KEY_FILE: &str = "register_signing_key"; + +pub fn get_secret_key() -> Result { + // try env var first + let why_env_failed = match env::var(SECRET_KEY) { + Ok(key) => return Ok(key), + Err(e) => e, + }; + + // try from data dir + let dir = get_client_data_dir_path() + .wrap_err(format!("Failed to obtain secret key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) + .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY} env var"))?; + + // load the key from file + let key_path = dir.join(SECRET_KEY_FILE); + fs::read_to_string(&key_path) + .wrap_err("Failed to read secret key from file".to_string()) + .with_suggestion(|| format!("make sure you've provided the {SECRET_KEY} env var or have the key in a file at {key_path:?}")) +} + +pub fn get_register_signing_key() -> Result { + // try env var first + let why_env_failed = match env::var(REGISTER_SIGNING_KEY) { + Ok(key) => return Ok(key), + Err(e) => e, + }; + + // try from data dir + let dir = get_client_data_dir_path() + .wrap_err(format!("Failed to obtain register signing key from env var: {why_env_failed}, reading from disk also failed as couldn't access data dir")) + .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY} env var"))?; + + // load the key from file + let key_path = dir.join(REGISTER_SIGNING_KEY_FILE); + fs::read_to_string(&key_path) + .wrap_err("Failed to read secret key from file".to_string()) + .with_suggestion(|| format!("make sure you've provided the {REGISTER_SIGNING_KEY} env var or have the key in a file at {key_path:?}")) +} + +pub fn get_client_data_dir_path() -> Result { + let mut home_dirs = dirs_next::data_dir() + .ok_or_else(|| eyre!("Failed to obtain data dir, your OS might not be supported."))?; + home_dirs.push("safe"); + home_dirs.push("client"); + std::fs::create_dir_all(home_dirs.as_path()) + .wrap_err("Failed to create data dir".to_string())?; + Ok(home_dirs) +} + +pub async fn get_peers(peers: PeersArgs) -> Result> { + peers.get_peers().await + .wrap_err(format!("Please provide valid Network peers to connect to")) + .with_suggestion(|| format!("make sure you've provided network peers using the --peers option or the {SAFE_PEERS_ENV} env var")) + .with_suggestion(|| format!("a peer address looks like this: /ip4/42.42.42.42/udp/4242/quic-v1/p2p/B64nodePeerIDvdjb3FAJF4ks3moreBase64CharsHere")) +} + +pub(crate) fn get_evm_network() -> Result { + // NB TODO load custom network from config file/env/cmd line + let network = Network::ArbitrumOne; + Ok(network) +}