From 275da9d943750598e4ed95769a89cc53e4afa7cb Mon Sep 17 00:00:00 2001 From: Elliot Voris Date: Thu, 18 Jul 2024 09:37:27 -0500 Subject: [PATCH 01/11] docs: reformat link to ledger headers in `events.rs` (#1468) For better appearance in the CLI, and to still work with MDX v3, this change removes the link definition, in favor of the bare URL. I'm also adding a macro for clippy to ignore the "improper markdown" in this comment. --- FULL_HELP_DOCS.md | 4 +--- cmd/soroban-cli/src/commands/events.rs | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index e2b310aea..61766e3f0 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -627,9 +627,7 @@ Watch the network for contract events ###### **Options:** -* `--start-ledger ` — The first [ledger sequence] number in the range to pull events - - [ledger sequence]: https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence +* `--start-ledger ` — The first ledger sequence number in the range to pull events https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence * `--cursor ` — The cursor corresponding to the start of the event range * `--output ` — Output formatting options for event stream diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index 63bdfa04d..e1013eada 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -12,9 +12,9 @@ use crate::rpc; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// The first [ledger sequence] number in the range to pull events - /// - /// [ledger sequence]: https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence + #[allow(clippy::doc_markdown)] + /// The first ledger sequence number in the range to pull events + /// https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence #[arg(long, conflicts_with = "cursor", required_unless_present = "cursor")] start_ledger: Option, /// The cursor corresponding to the start of the event range. From 76eab63612c3bb7e062d3786a4223e874d558813 Mon Sep 17 00:00:00 2001 From: Elliot Voris Date: Thu, 18 Jul 2024 10:36:55 -0500 Subject: [PATCH 02/11] docs: correct the URL to the ledger headers page (#1469) The ledger headers page has moved, and this commit uses the new location. --- FULL_HELP_DOCS.md | 2 +- cmd/soroban-cli/src/commands/events.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 61766e3f0..aa17b6949 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -627,7 +627,7 @@ Watch the network for contract events ###### **Options:** -* `--start-ledger ` — The first ledger sequence number in the range to pull events https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence +* `--start-ledger ` — The first ledger sequence number in the range to pull events https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/ledger-headers#ledger-sequence * `--cursor ` — The cursor corresponding to the start of the event range * `--output ` — Output formatting options for event stream diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index e1013eada..e9f3ba645 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -14,7 +14,7 @@ use crate::rpc; pub struct Cmd { #[allow(clippy::doc_markdown)] /// The first ledger sequence number in the range to pull events - /// https://developers.stellar.org/docs/encyclopedia/ledger-headers#ledger-sequence + /// https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/ledger-headers#ledger-sequence #[arg(long, conflicts_with = "cursor", required_unless_present = "cursor")] start_ledger: Option, /// The cursor corresponding to the start of the event range. From 41a1e0dd0e3e8e34f94b4e78fc7f71d7f15420c7 Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Thu, 18 Jul 2024 14:31:12 -0400 Subject: [PATCH 03/11] fix durability not setting default (#1470) * fix durability not setting default * doc update --- FULL_HELP_DOCS.md | 6 +++--- .../soroban-test/tests/it/integration/hello_world.rs | 12 ++++++++++++ cmd/soroban-cli/src/key.rs | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index aa17b6949..689ce3036 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -266,7 +266,7 @@ Extend the time to live ledger of a contract-data ledger entry. If no keys are specified the contract itself is extended. -**Usage:** `stellar contract extend [OPTIONS] --ledgers-to-extend --durability --source-account ` +**Usage:** `stellar contract extend [OPTIONS] --ledgers-to-extend --source-account ` ###### **Options:** @@ -534,7 +534,7 @@ Optimize a WASM file Print the current value of a contract-data ledger entry -**Usage:** `stellar contract read [OPTIONS] --durability --source-account ` +**Usage:** `stellar contract read [OPTIONS] --source-account ` ###### **Options:** @@ -581,7 +581,7 @@ Restore an evicted value for a contract-data legder entry. If no keys are specificed the contract itself is restored. -**Usage:** `stellar contract restore [OPTIONS] --durability --source-account ` +**Usage:** `stellar contract restore [OPTIONS] --source-account ` ###### **Options:** diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index 16c13dd25..ec0c843db 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -295,6 +295,18 @@ async fn contract_data_read() { .assert() .success() .stdout(predicates::str::starts_with("COUNTER,2")); + + // ensure default durability = persistent works + sandbox + .new_assert_cmd("contract") + .arg("read") + .arg("--id") + .arg(id) + .arg("--key") + .arg(KEY) + .assert() + .success() + .stdout(predicates::str::starts_with("COUNTER,2")); } #[tokio::test] diff --git a/cmd/soroban-cli/src/key.rs b/cmd/soroban-cli/src/key.rs index 7a0ccb844..885295936 100644 --- a/cmd/soroban-cli/src/key.rs +++ b/cmd/soroban-cli/src/key.rs @@ -56,7 +56,7 @@ pub struct Args { )] pub wasm_hash: Option, /// Storage entry durability - #[arg(long, value_enum, required = true, default_value = "persistent")] + #[arg(long, value_enum, default_value = "persistent")] pub durability: Durability, } From 3fbf97913ef6393a43eb8303a7ff1affd85f36c4 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:18:21 -0400 Subject: [PATCH 04/11] Fix/network container improvements (#1423) * Return a better error message when stopping container that doesn't exist * Fix doc strings for network container commands * Allow for overwriting the default container name with -c --------- Co-authored-by: Willem Wyndham --- FULL_HELP_DOCS.md | 41 ++- .../src/commands/network/container.rs | 10 +- .../src/commands/network/container/logs.rs | 18 +- .../src/commands/network/container/shared.rs | 137 +++++---- .../src/commands/network/container/start.rs | 261 ++++++++++-------- .../src/commands/network/container/stop.rs | 57 +++- cmd/soroban-cli/src/commands/network/mod.rs | 2 +- 7 files changed, 295 insertions(+), 231 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 689ce3036..035175ad6 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1060,7 +1060,7 @@ Start a container running a Stellar node, RPC, API, and friendbot (faucet). By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command: -`docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable-soroban-rpc` +`docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable rpc,horizon` **Usage:** `stellar network start [OPTIONS] ` @@ -1074,6 +1074,7 @@ By default, when starting a testnet container, without any optional arguments, i ###### **Options:** * `-d`, `--docker-host ` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock +* `--name ` — Optional argument to specify the container name * `-l`, `--limits ` — Optional argument to specify the limits for the local network only * `-p`, `--ports-mapping ` — Argument to specify the `HOST_PORT:CONTAINER_PORT` mapping @@ -1089,14 +1090,11 @@ By default, when starting a testnet container, without any optional arguments, i Stop a network started with `network start`. For example, if you ran `stellar network start local`, you can use `stellar network stop local` to stop it. -**Usage:** `stellar network stop [OPTIONS] ` +**Usage:** `stellar network stop [OPTIONS] ` ###### **Arguments:** -* `` — Network to stop - - Possible values: `local`, `testnet`, `futurenet`, `pubnet` - +* `` — Container to stop ###### **Options:** @@ -1112,24 +1110,21 @@ Commands to start, stop and get logs for a quickstart container ###### **Subcommands:** -* `logs` — Tail logs of a running network container -* `start` — Start network -* `stop` — Stop a network started with `network container start`. For example, if you ran `network container start local`, you can use `network container stop local` to stop it +* `logs` — Get logs from a running network container +* `start` — Start a container running a Stellar node, RPC, API, and friendbot (faucet) +* `stop` — Stop a network container started with `network container start` ## `stellar network container logs` -Tail logs of a running network container +Get logs from a running network container -**Usage:** `stellar network container logs [OPTIONS] ` +**Usage:** `stellar network container logs [OPTIONS] ` ###### **Arguments:** -* `` — Network to tail - - Possible values: `local`, `testnet`, `futurenet`, `pubnet` - +* `` — Container to get logs from ###### **Options:** @@ -1139,15 +1134,13 @@ Tail logs of a running network container ## `stellar network container start` -Start network - Start a container running a Stellar node, RPC, API, and friendbot (faucet). -`stellar network start NETWORK [OPTIONS]` +`stellar network container start NETWORK [OPTIONS]` By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command: -`docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable-soroban-rpc` +`docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable rpc,horizon` **Usage:** `stellar network container start [OPTIONS] ` @@ -1161,6 +1154,7 @@ By default, when starting a testnet container, without any optional arguments, i ###### **Options:** * `-d`, `--docker-host ` — Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock +* `--name ` — Optional argument to specify the container name * `-l`, `--limits ` — Optional argument to specify the limits for the local network only * `-p`, `--ports-mapping ` — Argument to specify the `HOST_PORT:CONTAINER_PORT` mapping @@ -1172,16 +1166,13 @@ By default, when starting a testnet container, without any optional arguments, i ## `stellar network container stop` -Stop a network started with `network container start`. For example, if you ran `network container start local`, you can use `network container stop local` to stop it +Stop a network container started with `network container start` -**Usage:** `stellar network container stop [OPTIONS] ` +**Usage:** `stellar network container stop [OPTIONS] ` ###### **Arguments:** -* `` — Network to stop - - Possible values: `local`, `testnet`, `futurenet`, `pubnet` - +* `` — Container to stop ###### **Options:** diff --git a/cmd/soroban-cli/src/commands/network/container.rs b/cmd/soroban-cli/src/commands/network/container.rs index 16e5d73be..511c0e11b 100644 --- a/cmd/soroban-cli/src/commands/network/container.rs +++ b/cmd/soroban-cli/src/commands/network/container.rs @@ -10,19 +10,17 @@ pub type StopCmd = stop::Cmd; #[derive(Debug, clap::Subcommand)] pub enum Cmd { - /// Tail logs of a running network container + /// Get logs from a running network container Logs(logs::Cmd), - /// Start network - /// /// Start a container running a Stellar node, RPC, API, and friendbot (faucet). /// - /// `stellar network start NETWORK [OPTIONS]` + /// `stellar network container start NETWORK [OPTIONS]` /// /// By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command: /// - /// `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable-soroban-rpc` + /// `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable rpc,horizon` Start(start::Cmd), - /// Stop a network started with `network container start`. For example, if you ran `network container start local`, you can use `network container stop local` to stop it. + /// Stop a network container started with `network container start`. Stop(stop::Cmd), } diff --git a/cmd/soroban-cli/src/commands/network/container/logs.rs b/cmd/soroban-cli/src/commands/network/container/logs.rs index e37ceb098..99b36af9b 100644 --- a/cmd/soroban-cli/src/commands/network/container/logs.rs +++ b/cmd/soroban-cli/src/commands/network/container/logs.rs @@ -1,8 +1,8 @@ use futures_util::TryStreamExt; -use crate::commands::network::container::shared::{ - connect_to_docker, Error as ConnectionError, Network, DOCKER_HOST_HELP, -}; +use crate::commands::network::container::shared::Error as ConnectionError; + +use super::shared::{Args, Name}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -15,17 +15,17 @@ pub enum Error { #[derive(Debug, clap::Parser, Clone)] pub struct Cmd { - /// Network to tail - pub network: Network, + #[command(flatten)] + pub container_args: Args, - #[arg(short = 'd', long, help = DOCKER_HOST_HELP, env = "DOCKER_HOST")] - pub docker_host: Option, + /// Container to get logs from + pub name: String, } impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let container_name = format!("stellar-{}", self.network); - let docker = connect_to_docker(&self.docker_host).await?; + let container_name = Name(self.name.clone()).get_internal_container_name(); + let docker = self.container_args.connect_to_docker().await?; let logs_stream = &mut docker.logs( &container_name, Some(bollard::container::LogsOptions { diff --git a/cmd/soroban-cli/src/commands/network/container/shared.rs b/cmd/soroban-cli/src/commands/network/container/shared.rs index 9da8a1bea..f819f3ed3 100644 --- a/cmd/soroban-cli/src/commands/network/container/shared.rs +++ b/cmd/soroban-cli/src/commands/network/container/shared.rs @@ -31,7 +31,81 @@ pub enum Error { UnsupportedURISchemeError { uri: String }, } -#[derive(ValueEnum, Debug, Clone, PartialEq)] +#[derive(Debug, clap::Parser, Clone)] +pub struct Args { + /// Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock + #[arg(short = 'd', long, help = DOCKER_HOST_HELP, env = "DOCKER_HOST")] + pub docker_host: Option, +} + +impl Args { + pub(crate) fn get_additional_flags(&self) -> String { + self.docker_host + .as_ref() + .map(|docker_host| format!("--docker-host {docker_host}")) + .unwrap_or_default() + } + + pub(crate) async fn connect_to_docker(&self) -> Result { + // if no docker_host is provided, use the default docker host: + // "unix:///var/run/docker.sock" on unix machines + // "npipe:////./pipe/docker_engine" on windows machines + let host = self.docker_host.as_ref().map_or_else( + || DEFAULT_DOCKER_HOST.to_string(), + std::string::ToString::to_string, + ); + + // this is based on the `connect_with_defaults` method which has not yet been released in the bollard crate + // https://github.com/fussybeaver/bollard/blob/0972b1aac0ad5c08798e100319ddd0d2ee010365/src/docker.rs#L660 + let connection = match host.clone() { + // if tcp or http, use connect_with_http_defaults + // if unix and host starts with "unix://" use connect_with_unix + // if windows and host starts with "npipe://", use connect_with_named_pipe + // else default to connect_with_unix + h if h.starts_with("tcp://") || h.starts_with("http://") => { + Docker::connect_with_http_defaults() + } + #[cfg(unix)] + h if h.starts_with("unix://") => { + Docker::connect_with_unix(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION) + } + #[cfg(windows)] + h if h.starts_with("npipe://") => { + Docker::connect_with_named_pipe(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION) + } + _ => { + return Err(Error::UnsupportedURISchemeError { + uri: host.to_string(), + }); + } + }?; + + match check_docker_connection(&connection).await { + Ok(()) => Ok(connection), + // If we aren't able to connect with the defaults, or with the provided docker_host + // try to connect with the default docker desktop socket since that is a common use case for devs + #[allow(unused_variables)] + Err(e) => { + // if on unix, try to connect to the default docker desktop socket + #[cfg(unix)] + { + let docker_desktop_connection = try_docker_desktop_socket(&host)?; + match check_docker_connection(&docker_desktop_connection).await { + Ok(()) => Ok(docker_desktop_connection), + Err(err) => Err(err)?, + } + } + + #[cfg(windows)] + { + Err(e)? + } + } + } + } +} + +#[derive(ValueEnum, Debug, Copy, Clone, PartialEq)] pub enum Network { Local, Testnet, @@ -52,61 +126,14 @@ impl fmt::Display for Network { } } -pub async fn connect_to_docker(docker_host: &Option) -> Result { - // if no docker_host is provided, use the default docker host: - // "unix:///var/run/docker.sock" on unix machines - // "npipe:////./pipe/docker_engine" on windows machines - - let host = docker_host - .clone() - .unwrap_or(DEFAULT_DOCKER_HOST.to_string()); - - // this is based on the `connect_with_defaults` method which has not yet been released in the bollard crate - // https://github.com/fussybeaver/bollard/blob/0972b1aac0ad5c08798e100319ddd0d2ee010365/src/docker.rs#L660 - let connection = match host.clone() { - // if tcp or http, use connect_with_http_defaults - // if unix and host starts with "unix://" use connect_with_unix - // if windows and host starts with "npipe://", use connect_with_named_pipe - // else default to connect_with_unix - h if h.starts_with("tcp://") || h.starts_with("http://") => { - Docker::connect_with_http_defaults() - } - #[cfg(unix)] - h if h.starts_with("unix://") => { - Docker::connect_with_unix(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION) - } - #[cfg(windows)] - h if h.starts_with("npipe://") => { - Docker::connect_with_named_pipe(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION) - } - _ => { - return Err(Error::UnsupportedURISchemeError { - uri: host.to_string(), - }); - } - }?; - - match check_docker_connection(&connection).await { - Ok(()) => Ok(connection), - // If we aren't able to connect with the defaults, or with the provided docker_host - // try to connect with the default docker desktop socket since that is a common use case for devs - #[allow(unused_variables)] - Err(e) => { - // if on unix, try to connect to the default docker desktop socket - #[cfg(unix)] - { - let docker_desktop_connection = try_docker_desktop_socket(&host)?; - match check_docker_connection(&docker_desktop_connection).await { - Ok(()) => Ok(docker_desktop_connection), - Err(err) => Err(err)?, - } - } +pub struct Name(pub String); +impl Name { + pub fn get_internal_container_name(&self) -> String { + format!("stellar-{}", self.0) + } - #[cfg(windows)] - { - Err(e)? - } - } + pub fn get_external_container_name(&self) -> String { + self.0.to_string() } } diff --git a/cmd/soroban-cli/src/commands/network/container/start.rs b/cmd/soroban-cli/src/commands/network/container/start.rs index cc9619f1d..46348c297 100644 --- a/cmd/soroban-cli/src/commands/network/container/start.rs +++ b/cmd/soroban-cli/src/commands/network/container/start.rs @@ -7,9 +7,9 @@ use bollard::{ }; use futures_util::TryStreamExt; -use crate::commands::network::container::shared::{ - connect_to_docker, Error as ConnectionError, Network, DOCKER_HOST_HELP, -}; +use crate::commands::network::container::shared::{Error as ConnectionError, Network}; + +use super::shared::{Args, Name}; const DEFAULT_PORT_MAPPING: &str = "8000:8000"; const DOCKER_IMAGE: &str = "docker.io/stellar/quickstart"; @@ -17,19 +17,23 @@ const DOCKER_IMAGE: &str = "docker.io/stellar/quickstart"; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("⛔ ️Failed to connect to docker: {0}")] - ConnectionError(#[from] ConnectionError), + DockerConnectionFailed(#[from] ConnectionError), #[error("⛔ ️Failed to create container: {0}")] - BollardErr(#[from] bollard::errors::Error), + CreateContainerFailed(#[from] bollard::errors::Error), } #[derive(Debug, clap::Parser, Clone)] pub struct Cmd { + #[command(flatten)] + pub container_args: Args, + /// Network to start pub network: Network, - #[arg(short = 'd', long, help = DOCKER_HOST_HELP, env = "DOCKER_HOST")] - pub docker_host: Option, + /// Optional argument to specify the container name + #[arg(long)] + pub name: Option, /// Optional argument to specify the limits for the local network only #[arg(short = 'l', long)] @@ -51,137 +55,152 @@ pub struct Cmd { impl Cmd { pub async fn run(&self) -> Result<(), Error> { println!("ℹ️ Starting {} network", &self.network); - run_docker_command(self).await + self.run_docker_command().await } -} -async fn run_docker_command(cmd: &Cmd) -> Result<(), Error> { - let docker = connect_to_docker(&cmd.docker_host).await?; - - let image = get_image_name(cmd); - docker - .create_image( - Some(CreateImageOptions { - from_image: image.clone(), + async fn run_docker_command(&self) -> Result<(), Error> { + let docker = self.container_args.connect_to_docker().await?; + + let image = self.get_image_name(); + docker + .create_image( + Some(CreateImageOptions { + from_image: image.clone(), + ..Default::default() + }), + None, + None, + ) + .try_collect::>() + .await?; + + let config = Config { + image: Some(image), + cmd: Some(self.get_container_args()), + attach_stdout: Some(true), + attach_stderr: Some(true), + host_config: Some(HostConfig { + auto_remove: Some(true), + port_bindings: Some(self.get_port_mapping()), ..Default::default() }), - None, - None, - ) - .try_collect::>() - .await?; - - let container_args = get_container_args(cmd); - let port_mapping = get_port_mapping(cmd); - - let config = Config { - image: Some(image), - cmd: Some(container_args), - attach_stdout: Some(true), - attach_stderr: Some(true), - host_config: Some(HostConfig { - auto_remove: Some(true), - port_bindings: Some(port_mapping), ..Default::default() - }), - ..Default::default() - }; - - let container_name = format!("stellar-{}", cmd.network); - let create_container_response = docker - .create_container( - Some(CreateContainerOptions { - name: container_name.clone(), - ..Default::default() - }), - config, - ) - .await?; - - docker - .start_container( - &create_container_response.id, - None::>, - ) - .await?; - println!("✅ Container started: {container_name}"); - let stop_message = format!( - "ℹ️ To stop this container run: stellar network stop {network} {additional_flags}", - network = &cmd.network, - additional_flags = if cmd.docker_host.is_some() { - format!("--docker-host {}", cmd.docker_host.as_ref().unwrap()) - } else { - String::new() + }; + + let create_container_response = docker + .create_container( + Some(CreateContainerOptions { + name: self.container_name().get_internal_container_name(), + ..Default::default() + }), + config, + ) + .await?; + + docker + .start_container( + &create_container_response.id, + None::>, + ) + .await?; + println!( + "✅ Container started: {}", + self.container_name().get_external_container_name() + ); + self.print_log_message(); + self.print_stop_message(); + Ok(()) + } + + fn get_image_name(&self) -> String { + // this can be overriden with the `-t` flag + let mut image_tag = match &self.network { + Network::Pubnet => "latest", + Network::Futurenet => "future", + _ => "testing", // default to testing for local and testnet + }; + + if let Some(image_override) = &self.image_tag_override { + println!( + "Overriding docker image tag to use '{image_override}' instead of '{image_tag}'" + ); + image_tag = image_override; } - ); - println!("{stop_message}"); - Ok(()) -} + format!("{DOCKER_IMAGE}:{image_tag}") + } -fn get_container_args(cmd: &Cmd) -> Vec { - [ - format!("--{}", cmd.network), - "--enable rpc,horizon".to_string(), - get_protocol_version_arg(cmd), - get_limits_arg(cmd), - ] - .iter() - .filter(|&s| !s.is_empty()) - .cloned() - .collect() -} + fn get_container_args(&self) -> Vec { + [ + format!("--{}", self.network), + "--enable rpc,horizon".to_string(), + self.get_protocol_version_arg(), + self.get_limits_arg(), + ] + .iter() + .filter(|&s| !s.is_empty()) + .cloned() + .collect() + } + + // The port mapping in the bollard crate is formatted differently than the docker CLI. In the docker CLI, we usually specify exposed ports as `-p HOST_PORT:CONTAINER_PORT`. But with the bollard crate, it is expecting the port mapping to be a map of the container port (with the protocol) to the host port. + fn get_port_mapping(&self) -> HashMap>> { + let mut port_mapping_hash = HashMap::new(); + for port_mapping in &self.ports_mapping { + let ports_vec: Vec<&str> = port_mapping.split(':').collect(); + let from_port = ports_vec[0]; + let to_port = ports_vec[1]; + + port_mapping_hash.insert( + format!("{to_port}/tcp"), + Some(vec![PortBinding { + host_ip: None, + host_port: Some(from_port.to_string()), + }]), + ); + } -fn get_image_name(cmd: &Cmd) -> String { - // this can be overriden with the `-t` flag - let mut image_tag = match cmd.network { - Network::Pubnet => "latest", - Network::Futurenet => "future", - _ => "testing", // default to testing for local and testnet - }; - - if let Some(image_override) = &cmd.image_tag_override { - println!("Overriding docker image tag to use '{image_override}' instead of '{image_tag}'"); - image_tag = image_override; + port_mapping_hash } - format!("{DOCKER_IMAGE}:{image_tag}") -} + fn container_name(&self) -> Name { + Name(self.name.clone().unwrap_or(self.network.to_string())) + } -// The port mapping in the bollard crate is formatted differently than the docker CLI. In the docker CLI, we usually specify exposed ports as `-p HOST_PORT:CONTAINER_PORT`. But with the bollard crate, it is expecting the port mapping to be a map of the container port (with the protocol) to the host port. -fn get_port_mapping(cmd: &Cmd) -> HashMap>> { - let mut port_mapping_hash = HashMap::new(); - for port_mapping in &cmd.ports_mapping { - let ports_vec: Vec<&str> = port_mapping.split(':').collect(); - let from_port = ports_vec[0]; - let to_port = ports_vec[1]; - - port_mapping_hash.insert( - format!("{to_port}/tcp"), - Some(vec![PortBinding { - host_ip: None, - host_port: Some(from_port.to_string()), - }]), + fn print_log_message(&self) { + let log_message = format!( + "ℹ️ To see the logs for this container run: stellar network container logs {container_name} {additional_flags}", + container_name = self.container_name().get_external_container_name(), + additional_flags = self.container_args.get_additional_flags(), ); + println!("{log_message}"); } - port_mapping_hash -} + fn print_stop_message(&self) { + let stop_message = + format!( + "ℹ️ To stop this container run: stellar network container stop {container_name} {additional_flags}", + container_name = self.container_name().get_external_container_name(), + additional_flags = self.container_args.get_additional_flags(), + ); + println!("{stop_message}"); + } -fn get_protocol_version_arg(cmd: &Cmd) -> String { - if cmd.network == Network::Local && cmd.protocol_version.is_some() { - let version = cmd.protocol_version.as_ref().unwrap(); - format!("--protocol-version {version}") - } else { - String::new() + fn get_protocol_version_arg(&self) -> String { + if self.network == Network::Local && self.protocol_version.is_some() { + let version = self.protocol_version.as_ref().unwrap(); + format!("--protocol-version {version}") + } else { + String::new() + } } -} -fn get_limits_arg(cmd: &Cmd) -> String { - if cmd.network == Network::Local && cmd.limits.is_some() { - let limits = cmd.limits.as_ref().unwrap(); - format!("--limits {limits}") - } else { - String::new() + fn get_limits_arg(&self) -> String { + if self.network == Network::Local && self.limits.is_some() { + let limits = self.limits.as_ref().unwrap(); + format!("--limits {limits}") + } else { + String::new() + } } } diff --git a/cmd/soroban-cli/src/commands/network/container/stop.rs b/cmd/soroban-cli/src/commands/network/container/stop.rs index c511b5e4d..733c86436 100644 --- a/cmd/soroban-cli/src/commands/network/container/stop.rs +++ b/cmd/soroban-cli/src/commands/network/container/stop.rs @@ -1,29 +1,58 @@ -use crate::commands::network::container::shared::{ - connect_to_docker, Error as ConnectionError, Network, DOCKER_HOST_HELP, -}; +use crate::commands::network::container::shared::Error as BollardConnectionError; + +use super::shared::{Args, Name}; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("Failed to stop container: {0}")] - StopContainerError(#[from] ConnectionError), + #[error("⛔ Failed to connect to docker: {0}")] + DockerConnectionFailed(#[from] BollardConnectionError), + + #[error("⛔ Container {container_name} not found")] + ContainerNotFound { + container_name: String, + #[source] + source: bollard::errors::Error, + }, + + #[error("⛔ Failed to stop container: {0}")] + ContainerStopFailed(#[from] bollard::errors::Error), } #[derive(Debug, clap::Parser, Clone)] pub struct Cmd { - /// Network to stop - pub network: Network, + #[command(flatten)] + pub container_args: Args, - #[arg(short = 'd', long, help = DOCKER_HOST_HELP, env = "DOCKER_HOST")] - pub docker_host: Option, + /// Container to stop + pub name: String, } impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let container_name = format!("stellar-{}", self.network); - let docker = connect_to_docker(&self.docker_host).await?; - println!("ℹ️ Stopping container: {container_name}"); - docker.stop_container(&container_name, None).await.unwrap(); - println!("✅ Container stopped: {container_name}"); + let container_name = Name(self.name.clone()); + let docker = self.container_args.connect_to_docker().await?; + println!( + "ℹ️ Stopping container: {}", + container_name.get_external_container_name() + ); + docker + .stop_container(&container_name.get_internal_container_name(), None) + .await + .map_err(|e| { + let msg = e.to_string(); + if msg.contains("No such container") { + Error::ContainerNotFound { + container_name: container_name.get_external_container_name(), + source: e, + } + } else { + Error::ContainerStopFailed(e) + } + })?; + println!( + "✅ Container stopped: {}", + container_name.get_external_container_name() + ); Ok(()) } } diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 435492295..1c34f92c8 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -38,7 +38,7 @@ pub enum Cmd { /// /// By default, when starting a testnet container, without any optional arguments, it will run the equivalent of the following docker command: /// - /// `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable-soroban-rpc` + /// `docker run --rm -p 8000:8000 --name stellar stellar/quickstart:testing --testnet --enable rpc,horizon` Start(container::StartCmd), /// ⚠️ Deprecated: use `stellar container stop` instead /// From 2b19b7a9f5ed7a724dd2ad63a548ddfd6b0e951a Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Mon, 22 Jul 2024 22:10:08 -0400 Subject: [PATCH 05/11] add tx hash command (#1430) * add tx hash command * docs * Update cmd/soroban-cli/src/commands/tx/mod.rs more detailed doc Co-authored-by: Willem Wyndham * docs update * fix: use network only --------- Co-authored-by: Willem Wyndham --- FULL_HELP_DOCS.md | 15 +++++++++ .../soroban-test/tests/it/integration/tx.rs | 19 +++++++++++ cmd/soroban-cli/src/commands/tx/hash.rs | 33 +++++++++++++++++++ cmd/soroban-cli/src/commands/tx/mod.rs | 7 ++++ 4 files changed, 74 insertions(+) create mode 100644 cmd/soroban-cli/src/commands/tx/hash.rs diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 035175ad6..8dea0e36b 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -1197,6 +1197,7 @@ Sign, Simulate, and Send transactions ###### **Subcommands:** * `simulate` — Simulate a transaction envelope from stdin +* `hash` — Calculate the hash of a transaction envelope from stdin @@ -1218,6 +1219,20 @@ Simulate a transaction envelope from stdin +## `stellar tx hash` + +Calculate the hash of a transaction envelope from stdin + +**Usage:** `stellar tx hash [OPTIONS]` + +###### **Options:** + +* `--rpc-url ` — RPC server endpoint +* `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server +* `--network ` — Name of network to use from config + + + ## `stellar cache` Cache for transactions and contract specs diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 9f5204a8d..bcb880b18 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -30,3 +30,22 @@ async fn txn_simulate() { assembled_str ); } + +#[tokio::test] +async fn txn_hash() { + let sandbox = &TestEnv::new(); + + let xdr_base64 = "AAAAAgAAAACVk/0xt9tV/cUbF53iwQ3tkKLlq9zG2wV5qd9lRjZjlQAHt/sAFsKTAAAABAAAAAEAAAAAAAAAAAAAAABmOg6nAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABfcHs35M1GZ/JkY2+DHMs4dEUaqjynMnDYK/Gp0eulN8AAAAIdHJhbnNmZXIAAAADAAAAEgAAAAEFO1FR2Wg49QFY5KPOFAQ0bV5fN+7LD2GSQvOaHSH44QAAABIAAAAAAAAAAJWT/TG321X9xRsXneLBDe2QouWr3MbbBXmp32VGNmOVAAAACgAAAAAAAAAAAAAAADuaygAAAAABAAAAAQAAAAEFO1FR2Wg49QFY5KPOFAQ0bV5fN+7LD2GSQvOaHSH44QAAAY9SyLSVABbC/QAAABEAAAABAAAAAwAAAA8AAAASYXV0aGVudGljYXRvcl9kYXRhAAAAAAANAAAAJUmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjHQAAAAAAAAAAAAAPAAAAEGNsaWVudF9kYXRhX2pzb24AAAANAAAAcnsidHlwZSI6IndlYmF1dGhuLmdldCIsImNoYWxsZW5nZSI6ImhnMlRhOG8wWTliWFlyWlMyZjhzWk1kRFp6ektCSXhQNTZSd1FaNE90bTgiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjQ1MDcifQAAAAAADwAAAAlzaWduYXR1cmUAAAAAAAANAAAAQBcpuTFMxzkAdBs+5VIyJCBHaNuwEAva+kZVET4YuHVKF8gNII567RhxsnhBBSo5dDvssTN6vf2i42eEty66MtoAAAAAAAAAAX3B7N+TNRmfyZGNvgxzLOHRFGqo8pzJw2CvxqdHrpTfAAAACHRyYW5zZmVyAAAAAwAAABIAAAABBTtRUdloOPUBWOSjzhQENG1eXzfuyw9hkkLzmh0h+OEAAAASAAAAAAAAAACVk/0xt9tV/cUbF53iwQ3tkKLlq9zG2wV5qd9lRjZjlQAAAAoAAAAAAAAAAAAAAAA7msoAAAAAAAAAAAEAAAAAAAAAAwAAAAYAAAABfcHs35M1GZ/JkY2+DHMs4dEUaqjynMnDYK/Gp0eulN8AAAAUAAAAAQAAAAYAAAABBTtRUdloOPUBWOSjzhQENG1eXzfuyw9hkkLzmh0h+OEAAAAUAAAAAQAAAAeTiL4Gr2piUAmsXTev1ZzJ4kE2NUGZ0QMObd05iAMyzAAAAAMAAAAGAAAAAX3B7N+TNRmfyZGNvgxzLOHRFGqo8pzJw2CvxqdHrpTfAAAAEAAAAAEAAAACAAAADwAAAAdCYWxhbmNlAAAAABIAAAABBTtRUdloOPUBWOSjzhQENG1eXzfuyw9hkkLzmh0h+OEAAAABAAAAAAAAAACVk/0xt9tV/cUbF53iwQ3tkKLlq9zG2wV5qd9lRjZjlQAAAAYAAAABBTtRUdloOPUBWOSjzhQENG1eXzfuyw9hkkLzmh0h+OEAAAAVAAABj1LItJUAAAAAAEyTowAAGMgAAAG4AAAAAAADJBsAAAABRjZjlQAAAEASFnAIzNqpfdzv6yT0rSLMUDFgt7a/inCHurNCG55Jp8Imho04qRH+JNdkq0BgMC7yAJqH4N6Y2iGflFt3Lp4L"; + + let expected_hash = "bcc9fa60c8f6607c981d6e1c65d77ae07617720113f9080fe5883d8e4a331a68"; + + let hash = sandbox + .new_assert_cmd("tx") + .arg("hash") + .write_stdin(xdr_base64.as_bytes()) + .assert() + .success() + .stdout_as_str(); + + assert_eq!(hash.trim(), expected_hash); +} diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs new file mode 100644 index 000000000..d5b2c42e6 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -0,0 +1,33 @@ +use crate::{commands::global, utils::transaction_hash}; +use hex; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + TxEnvelopeFromStdin(#[from] super::xdr::Error), + #[error(transparent)] + XdrToBase64(#[from] soroban_env_host::xdr::Error), + #[error(transparent)] + Config(#[from] super::super::network::Error), +} + +// Command to return the transaction hash submitted to a network +/// e.g. `cat file.txt | soroban tx hash` +#[derive(Debug, clap::Parser, Clone, Default)] +#[group(skip)] +pub struct Cmd { + #[clap(flatten)] + pub network: super::super::network::Args, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let tx = super::xdr::unwrap_envelope_v1(super::xdr::tx_envelope_from_stdin()?)?; + let network = &self.network.get(&global_args.locator)?; + println!( + "{}", + hex::encode(transaction_hash(&tx, &network.network_passphrase)?) + ); + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 48161e3c7..59f07228a 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -2,6 +2,7 @@ use clap::Parser; use super::global; +pub mod hash; pub mod simulate; pub mod xdr; @@ -9,6 +10,8 @@ pub mod xdr; pub enum Cmd { /// Simulate a transaction envelope from stdin Simulate(simulate::Cmd), + /// Calculate the hash of a transaction envelope from stdin + Hash(hash::Cmd), } #[derive(thiserror::Error, Debug)] @@ -16,12 +19,16 @@ pub enum Error { /// An error during the simulation #[error(transparent)] Simulate(#[from] simulate::Error), + /// An error during hash calculation + #[error(transparent)] + Hash(#[from] hash::Error), } impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match self { Cmd::Simulate(cmd) => cmd.run(global_args).await?, + Cmd::Hash(cmd) => cmd.run(global_args)?, }; Ok(()) } From 31cdfdd53d60ae6f83ad1aece360e74b529f21af Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 23 Jul 2024 14:44:26 -0400 Subject: [PATCH 06/11] Feat/init overwrite (#1477) * adds overwrite option to init * add test case and update docs Co-authored-by: Willem Wyndham --- Cargo.lock | 1 + FULL_HELP_DOCS.md | 1 + cmd/soroban-cli/Cargo.toml | 1 + cmd/soroban-cli/src/commands/contract/init.rs | 155 ++++++++++++++---- 4 files changed, 127 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d70268b9..edab8bb9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4732,6 +4732,7 @@ dependencies = [ "tracing-subscriber", "ulid", "ureq", + "walkdir", "wasm-opt", "wasmparser 0.90.0", "which", diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 8dea0e36b..167549cf1 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -424,6 +424,7 @@ Initialize a Soroban project with an example contract * `-f`, `--frontend-template ` — An optional flag to pass in a url for a frontend template repository. Default value: `` +* `-o`, `--overwrite` — Overwrite all existing files. diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 3b0b90153..6d40ddd90 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -130,3 +130,4 @@ ureq = { version = "2.9.1", features = ["json"] } assert_cmd = "2.0.4" assert_fs = "1.0.7" predicates = "2.1.5" +walkdir = "2.5.0" diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index 21b9d48bb..26bf5816a 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -47,6 +47,9 @@ pub struct Cmd { long_help = "An optional flag to pass in a url for a frontend template repository." )] pub frontend_template: String, + + #[arg(short, long, long_help = "Overwrite all existing files.")] + pub overwrite: bool, } fn possible_example_values() -> ValueParser { @@ -89,7 +92,12 @@ impl Cmd { println!("ℹ️ Initializing project at {}", self.project_path); let project_path = Path::new(&self.project_path); - init(project_path, &self.frontend_template, &self.with_example)?; + init( + project_path, + &self.frontend_template, + &self.with_example, + self.overwrite, + )?; Ok(()) } @@ -103,13 +111,14 @@ fn init( project_path: &Path, frontend_template: &str, with_examples: &[String], + overwrite: bool, ) -> Result<(), Error> { // create a project dir, and copy the contents of the base template (contract-init-template) into it create_dir_all(project_path).map_err(|e| { eprintln!("Error creating new project directory: {project_path:?}"); e })?; - copy_template_files(project_path)?; + copy_template_files(project_path, overwrite)?; if !check_internet_connection() { println!("⚠️ It doesn't look like you're connected to the internet. We're still able to initialize a new project, but additional examples and the frontend template will not be included."); @@ -127,7 +136,7 @@ fn init( clone_repo(frontend_template, fe_template_dir.path())?; // copy the frontend template files into the project - copy_frontend_files(fe_template_dir.path(), project_path)?; + copy_frontend_files(fe_template_dir.path(), project_path, overwrite)?; } // if there are --with-example flags, include the example contracts @@ -142,17 +151,17 @@ fn init( clone_repo(SOROBAN_EXAMPLES_URL, examples_dir.path())?; // copy the example contracts into the project - copy_example_contracts(examples_dir.path(), project_path, with_examples)?; + copy_example_contracts(examples_dir.path(), project_path, with_examples, overwrite)?; } Ok(()) } -fn copy_template_files(project_path: &Path) -> Result<(), Error> { +fn copy_template_files(project_path: &Path, overwrite: bool) -> Result<(), Error> { for item in TemplateFiles::iter() { let mut to = project_path.join(item.as_ref()); - - if file_exists(&to) { + let exists = file_exists(&to); + if exists && !overwrite { println!( "ℹ️ Skipped creating {} as it already exists", &to.to_string_lossy() @@ -184,7 +193,11 @@ fn copy_template_files(project_path: &Path) -> Result<(), Error> { to = project_path.join(item_parent_path).join("Cargo.toml"); } - println!("➕ Writing {}", &to.to_string_lossy()); + if exists { + println!("🔄 Overwriting {}", &to.to_string_lossy()); + } else { + println!("➕ Writing {}", &to.to_string_lossy()); + } write(&to, file_contents).map_err(|e| { eprintln!("Error writing file: {to:?}"); e @@ -193,7 +206,7 @@ fn copy_template_files(project_path: &Path) -> Result<(), Error> { Ok(()) } -fn copy_contents(from: &Path, to: &Path) -> Result<(), Error> { +fn copy_contents(from: &Path, to: &Path, overwrite: bool) -> Result<(), Error> { let contents_to_exclude_from_copy = [ ".git", ".github", @@ -223,24 +236,26 @@ fn copy_contents(from: &Path, to: &Path) -> Result<(), Error> { eprintln!("Error creating directory: {new_path:?}"); e })?; - copy_contents(&path, &new_path)?; + copy_contents(&path, &new_path, overwrite)?; } else { - if file_exists(&new_path) { - if new_path.to_string_lossy().contains(".gitignore") { - append_contents(&path, &new_path)?; - } - if new_path.to_string_lossy().contains("README.md") { + let exists = file_exists(&new_path); + let new_path_str = new_path.to_string_lossy(); + if exists { + let append = + new_path_str.contains(".gitignore") || new_path_str.contains("README.md"); + if append { append_contents(&path, &new_path)?; } - println!( - "ℹ️ Skipped creating {} as it already exists", - &new_path.to_string_lossy() - ); - continue; + if overwrite && !append { + println!("🔄 Overwriting {new_path_str}"); + } else { + println!("ℹ️ Skipped creating {new_path_str} as it already exists"); + continue; + } + } else { + println!("➕ Writing {new_path_str}"); } - - println!("➕ Writing {}", &new_path.to_string_lossy()); copy(&path, &new_path).map_err(|e| { eprintln!( "Error copying from {:?} to {:?}", @@ -302,7 +317,12 @@ fn clone_repo(from_url: &str, to_path: &Path) -> Result<(), Error> { Ok(()) } -fn copy_example_contracts(from: &Path, to: &Path, contracts: &[String]) -> Result<(), Error> { +fn copy_example_contracts( + from: &Path, + to: &Path, + contracts: &[String], + overwrite: bool, +) -> Result<(), Error> { let project_contracts_path = to.join("contracts"); for contract in contracts { println!("ℹ️ Initializing example contract: {contract}"); @@ -315,7 +335,7 @@ fn copy_example_contracts(from: &Path, to: &Path, contracts: &[String]) -> Resul e })?; - copy_contents(&from_contract_path, &to_contract_path)?; + copy_contents(&from_contract_path, &to_contract_path, overwrite)?; edit_contract_cargo_file(&to_contract_path)?; } @@ -354,9 +374,9 @@ fn edit_contract_cargo_file(contract_path: &Path) -> Result<(), Error> { Ok(()) } -fn copy_frontend_files(from: &Path, to: &Path) -> Result<(), Error> { +fn copy_frontend_files(from: &Path, to: &Path, overwrite: bool) -> Result<(), Error> { println!("ℹ️ Initializing with frontend template"); - copy_contents(from, to)?; + copy_contents(from, to, overwrite)?; edit_package_json_files(to) } @@ -447,7 +467,13 @@ fn get_merged_file_delimiter(file_path: &Path) -> String { #[cfg(test)] mod tests { use itertools::Itertools; - use std::fs::{self, read_to_string}; + use std::{ + collections::HashMap, + fs::{self, read_to_string}, + path::PathBuf, + time::SystemTime, + }; + use walkdir::WalkDir; use super::*; @@ -458,7 +484,8 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); let with_examples = vec![]; - init(project_dir.as_path(), "", &with_examples).unwrap(); + let overwrite = false; + init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -476,7 +503,8 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); let with_examples = ["alloc".to_owned()]; - init(project_dir.as_path(), "", &with_examples).unwrap(); + let overwrite = false; + init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -499,7 +527,8 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("project"); let with_examples = ["account".to_owned(), "atomic_swap".to_owned()]; - init(project_dir.as_path(), "", &with_examples).unwrap(); + let overwrite = false; + init(project_dir.as_path(), "", &with_examples, overwrite).unwrap(); assert_base_template_files_exist(&project_dir); assert_default_hello_world_contract_files_exist(&project_dir); @@ -523,7 +552,8 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("project"); let with_examples = ["invalid_example".to_owned(), "atomic_swap".to_owned()]; - assert!(init(project_dir.as_path(), "", &with_examples,).is_err()); + let overwrite = false; + assert!(init(project_dir.as_path(), "", &with_examples, overwrite).is_err()); temp_dir.close().unwrap(); } @@ -533,10 +563,12 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); let with_examples = vec![]; + let overwrite = false; init( project_dir.as_path(), "https://github.com/stellar/soroban-astro-template", &with_examples, + overwrite, ) .unwrap(); @@ -556,15 +588,73 @@ mod tests { temp_dir.close().unwrap(); } + #[test] + fn test_init_with_overwrite() { + let temp_dir = tempfile::tempdir().unwrap(); + let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); + let with_examples = vec![]; + + // First initialization + init( + project_dir.as_path(), + "https://github.com/stellar/soroban-astro-template", + &with_examples, + false, + ) + .unwrap(); + + // Get initial modification times + let initial_mod_times = get_mod_times(&project_dir); + + // Second initialization with overwrite + init( + project_dir.as_path(), + "https://github.com/stellar/soroban-astro-template", + &with_examples, + true, // overwrite = true + ) + .unwrap(); + + // Get new modification times + let new_mod_times = get_mod_times(&project_dir); + + // Compare modification times + for (path, initial_time) in initial_mod_times { + let new_time = new_mod_times.get(&path).expect("File should still exist"); + assert!( + new_time > &initial_time, + "File {} should have a later modification time", + path.display() + ); + } + + temp_dir.close().unwrap(); + } + + fn get_mod_times(dir: &Path) -> HashMap { + let mut mod_times = HashMap::new(); + for entry in WalkDir::new(dir) { + let entry = entry.unwrap(); + if entry.file_type().is_file() { + let path = entry.path().to_owned(); + let metadata = fs::metadata(&path).unwrap(); + mod_times.insert(path, metadata.modified().unwrap()); + } + } + mod_times + } + #[test] fn test_init_from_within_an_existing_project() { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join("./"); let with_examples = vec![]; + let overwrite = false; init( project_dir.as_path(), "https://github.com/stellar/soroban-astro-template", &with_examples, + overwrite, ) .unwrap(); @@ -591,10 +681,12 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let project_dir = temp_dir.path().join(TEST_PROJECT_NAME); let with_examples = vec![]; + let overwrite = false; init( project_dir.as_path(), "https://github.com/stellar/soroban-astro-template", &with_examples, + overwrite, ) .unwrap(); @@ -603,6 +695,7 @@ mod tests { project_dir.as_path(), "https://github.com/stellar/soroban-astro-template", &with_examples, + overwrite, ) .unwrap(); From 3474c736d936999961c45874ddf2fdd0b4b09f4c Mon Sep 17 00:00:00 2001 From: Blaine Heffron Date: Tue, 23 Jul 2024 17:04:16 -0400 Subject: [PATCH 07/11] change fund to top off if balance below 10k xlm (#1478) * change fund to top off if balance below 10k xlm * cleanup * use unwrap_or_else * fix flakey test * to string instead of format --- Cargo.lock | 248 ++++++++++++++++++ .../tests/it/integration/hello_world.rs | 9 +- cmd/soroban-cli/Cargo.toml | 1 + .../src/commands/contract/deploy/asset.rs | 12 +- .../src/commands/contract/invoke.rs | 48 +--- cmd/soroban-cli/src/commands/keys/fund.rs | 187 ++++++++++++- cmd/soroban-cli/src/fee.rs | 14 +- cmd/soroban-cli/src/utils.rs | 54 +++- 8 files changed, 511 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edab8bb9a..eff278396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -117,6 +128,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -485,6 +502,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -560,6 +589,30 @@ dependencies = [ "serde_with", ] +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.39", + "syn_derive", +] + [[package]] name = "bstr" version = "1.9.1" @@ -586,6 +639,28 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -659,6 +734,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -1503,6 +1584,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -2481,6 +2568,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" @@ -3793,6 +3883,38 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -3808,6 +3930,26 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.33" @@ -3817,6 +3959,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -3926,6 +4074,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -3995,6 +4152,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -4050,6 +4236,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust_decimal" +version = "1.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4263,6 +4465,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.2" @@ -4556,6 +4764,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "similar" version = "2.5.0" @@ -4700,6 +4914,7 @@ dependencies = [ "regex", "rpassword", "rust-embed", + "rust_decimal", "sep5", "serde", "serde-aux", @@ -5219,6 +5434,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5246,6 +5473,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "temp-dir" version = "0.1.13" @@ -5834,6 +6067,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + [[package]] name = "valuable" version = "0.1.0" @@ -6305,6 +6544,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index ec0c843db..b6b6f5379 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -35,7 +35,7 @@ async fn invoke() { .arg("fund") .arg("test") .assert() - .stderr(predicates::str::contains("Account already exists")); + .stdout(predicates::str::contains("Nothing to do.")); sandbox .new_assert_cmd("keys") .arg("fund") @@ -138,6 +138,13 @@ async fn invoke() { handles_kebab_case(sandbox, id).await; fetch(sandbox, id).await; invoke_prng_u64_in_range_test(sandbox, id).await; + // test fund will add when account exists + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .assert() + .stdout(predicates::str::contains("New test balance:")); } fn invoke_hello_world(sandbox: &TestEnv, id: &str) { diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 6d40ddd90..0fdfa7494 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -100,6 +100,7 @@ pathdiff = "0.2.1" dotenvy = "0.15.7" directories = { workspace = true } ulid = { workspace = true, features = ["serde"] } +rust_decimal = "1.35.0" strum = "0.17.1" strum_macros = "0.17.1" gix = { version = "0.58.0", default-features = false, features = [ diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index e912d2b98..4d770d107 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -19,7 +19,7 @@ use crate::{ NetworkRunnable, }, rpc::{Client, Error as SorobanRpcError}, - utils::{contract_id_hash_from_asset, parsing::parse_asset}, + utils::{contract_id_hash_from_asset, get_account_details, parsing::parse_asset}, }; #[derive(thiserror::Error, Debug)] @@ -94,16 +94,10 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; - client - .verify_network_passphrase(Some(&network.network_passphrase)) - .await?; let key = config.key_pair()?; - - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); // TODO: use symbols for the method names (both here and in serve) - let account_details = client.get_account(&public_strkey).await?; + let account_details = + get_account_details(false, &client, &network.network_passphrase, &key).await?; let sequence: i64 = account_details.seq_num.into(); let network_passphrase = &network.network_passphrase; let contract_id = contract_id_hash_from_asset(&asset, network_passphrase)?; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 22462cbcb..74be03c0d 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,11 +12,10 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, AccountEntry, AccountEntryExt, AccountId, Hash, HostFunction, InvokeContractArgs, - InvokeHostFunctionOp, LedgerEntryData, Limits, Memo, MuxedAccount, Operation, - OperationBody, Preconditions, PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, - ScSpecTypeDef, ScVal, ScVec, SequenceNumber, String32, StringM, Thresholds, Transaction, - TransactionExt, Uint256, VecM, WriteXdr, + self, AccountId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, + LedgerEntryData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, + PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, + SequenceNumber, Transaction, TransactionExt, Uint256, VecM, WriteXdr, }, HostError, }; @@ -30,6 +29,7 @@ use super::super::{ use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; +use crate::utils::get_account_details; use crate::{ commands::{config::data, global, network}, rpc, Pwd, @@ -321,19 +321,14 @@ impl NetworkRunnable for Cmd { let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; } let client = rpc::Client::new(&network.rpc_url)?; - let account_details = if self.is_view { - default_account_entry() - } else { - client - .verify_network_passphrase(Some(&network.network_passphrase)) - .await?; - let key = config.key_pair()?; - - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - client.get_account(&public_strkey).await? - }; + let key = config.key_pair()?; + let account_details = get_account_details( + self.is_view, + &client.clone(), + &network.network_passphrase, + &key, + ) + .await?; let sequence: i64 = account_details.seq_num.into(); let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id; @@ -396,8 +391,6 @@ impl NetworkRunnable for Cmd { } } -const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); - // fn log_auth_cost_and_footprint(resources: Option<&SorobanResources>) { // if let Some(resources) = resources { // crate::log::footprint(&resources.footprint); @@ -426,21 +419,6 @@ const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519( // .unwrap_or_default() // } -fn default_account_entry() -> AccountEntry { - AccountEntry { - account_id: DEFAULT_ACCOUNT_ID, - balance: 0, - seq_num: SequenceNumber(0), - num_sub_entries: 0, - inflation_dest: None, - flags: 0, - home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }), - thresholds: Thresholds([0; 4]), - signers: unsafe { [].try_into().unwrap_unchecked() }, - ext: AccountEntryExt::V0, - } -} - pub fn output_to_string( spec: &Spec, res: &ScVal, diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index b6c088f13..5135d21bb 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -1,7 +1,18 @@ use clap::command; +use rand::{thread_rng, Rng}; +use rust_decimal::Decimal; +use soroban_sdk::xdr; +use stellar_strkey::ed25519::{PrivateKey, PublicKey}; -use crate::commands::network; +use crate::commands::config::secret::Secret; +use crate::commands::global; +use crate::commands::network::LOCAL_NETWORK_PASSPHRASE; +use crate::utils::contract_id_hash_from_asset; +use crate::utils::parsing::{self, parse_asset}; +use crate::{commands, rpc, CommandParser}; +use crate::{commands::network, utils::get_account_details}; +use super::super::config::secret; use super::address; #[derive(thiserror::Error, Debug)] @@ -10,6 +21,26 @@ pub enum Error { Address(#[from] address::Error), #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + Parsing(#[from] parsing::Error), + #[error(transparent)] + Rpc(#[from] rpc::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error(transparent)] + Clap(#[from] clap::Error), + #[error("RPC URL is missing in the network configuration")] + MissingRpcUrl, + #[error("Network passphrase is missing in the network configuration")] + MissingNetworkPassphrase, + #[error("Asset contract could not be deployed")] + AssetContractError, + #[error("Failed to transfer funds: {0}")] + FundTransferError(String), + #[error("Problem deploying asset contract: {0}")] + AssetDeploymentError(String), } #[derive(Debug, clap::Parser, Clone)] @@ -22,13 +53,159 @@ pub struct Cmd { pub address: address::Cmd, } +const STROOPS_PER_XLM: i64 = 10_000_000; +const DEFAULT_FRIENDBOT_AMOUNT: i64 = 10_000 * STROOPS_PER_XLM; + +fn convert_xlm_rounded(balance: i64) -> Decimal { + let xlm_balance = Decimal::new(balance, 0) / Decimal::new(STROOPS_PER_XLM, 0); + xlm_balance.round_dp(2) +} + impl Cmd { + pub async fn check_balance(&self) -> Result { + let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; + let network_passphrase = self + .network + .network_passphrase + .as_ref() + .ok_or(Error::MissingNetworkPassphrase)?; + let client = rpc::Client::new(rpc_url)?; + let key = self.address.private_key()?; + let account_details = + get_account_details(false, &client.clone(), network_passphrase, &key).await?; + Ok(account_details.balance) + } + + pub async fn create_temp_account(&self) -> Result<(PublicKey, PrivateKey), Error> { + let random_seed: String = thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) + .take(16) + .map(char::from) + .collect(); + let secret = Secret::from_seed(Some(&random_seed))?; + let addr = secret.public_key(self.address.hd_path)?; + let private_key = secret.private_key(self.address.hd_path)?; + let network = self.network.get(&self.address.locator)?; + network + .fund_address(&addr) + .await + .map_err(|e| { + tracing::warn!("fund_address failed: {e}"); + }) + .unwrap_or_default(); + Ok((addr, private_key)) + } + + pub async fn get_asset_contract_id(&self) -> Result { + // if network is local, deploy the Stellar Asset contract and retrieve ID + let network = self.network.get(&self.address.locator)?; + let asset = parse_asset("native")?; + let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; + if network.network_passphrase == LOCAL_NETWORK_PASSPHRASE { + let cmd = commands::contract::deploy::asset::Cmd::parse_arg_vec(&[ + "--asset", + "native", + "--source-account", + &self.address.name, + "--rpc-url", + rpc_url, + "--network-passphrase", + &network.network_passphrase, + ])?; + match cmd.run().await { + Ok(()) => { + let contract_id = + contract_id_hash_from_asset(&asset, &network.network_passphrase)?; + Ok(stellar_strkey::Contract(contract_id.0)) + } + Err(err) => { + if err.to_string().contains("ExistingValue") { + let contract_id = + contract_id_hash_from_asset(&asset, &network.network_passphrase)?; + Ok(stellar_strkey::Contract(contract_id.0)) + } else { + Err(Error::AssetDeploymentError(err.to_string())) + } + } + } + } else { + let contract_id = contract_id_hash_from_asset(&asset, &network.network_passphrase)?; + Ok(stellar_strkey::Contract(contract_id.0)) + } + } + + pub async fn add_funds(&self, amount: i64) -> Result<(), Error> { + let id = self.get_asset_contract_id().await?; + let addr = self.address.public_key()?; + let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; + let network_passphrase = self + .network + .network_passphrase + .as_ref() + .ok_or(Error::MissingNetworkPassphrase)?; + let (from_id, temp_secret) = self.create_temp_account().await?; + let cmd = commands::contract::invoke::Cmd::parse_arg_vec(&[ + "--id", + &id.to_string(), + "--source-account", + &temp_secret.to_string(), + "--rpc-url", + rpc_url, + "--network-passphrase", + network_passphrase, + "--", + "transfer", + "--to", + &addr.to_string(), + "--from", + &from_id.to_string(), + "--amount", + &amount.to_string(), + ])?; + cmd.run(&global::Args { + locator: self.address.locator.clone(), + ..Default::default() + }) + .await + .map_err(|e| Error::FundTransferError(e.to_string())) + } + pub async fn run(&self) -> Result<(), Error> { let addr = self.address.public_key()?; - self.network - .get(&self.address.locator)? - .fund_address(&addr) - .await?; + let balance = self.check_balance().await.unwrap_or_else(|err| { + eprintln!("Failed to check balance: {err}"); + 0 + }); + let rounded_xlm = convert_xlm_rounded(balance); + if balance >= DEFAULT_FRIENDBOT_AMOUNT { + println!( + "Current {} balance: {} XLM. Nothing to do.", + self.address.name, rounded_xlm, + ); + } else if balance == 0 { + self.network + .get(&self.address.locator)? + .fund_address(&addr) + .await?; + } else { + println!( + "Current {} balance: {} XLM. Topping off...", + self.address.name, rounded_xlm, + ); + self.add_funds(DEFAULT_FRIENDBOT_AMOUNT - balance).await?; + let balance = match self.check_balance().await { + Ok(balance) => balance, + Err(err) => { + eprintln!("Failed to check balance: {err}"); + 0 + } + }; + println!( + "New {} balance: {} XLM.", + self.address.name, + convert_xlm_rounded(balance), + ); + } Ok(()) } } diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 698d66007..0e7ccf580 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -27,11 +27,15 @@ pub struct Args { impl Args { pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Assembled { - if let Some(instructions) = self.instructions { - txn.set_max_instructions(instructions) - } else { - add_padding_to_instructions(txn) - } + apply_max_instructions_to_txn(txn, self.instructions) + } +} + +pub fn apply_max_instructions_to_txn(txn: Assembled, max_instructions: Option) -> Assembled { + if let Some(instructions) = max_instructions { + txn.set_max_instructions(instructions) + } else { + add_padding_to_instructions(txn) } } diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index e74222c2c..4ace5c906 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,16 +1,17 @@ -use ed25519_dalek::Signer; +use crate::rpc; +use ed25519_dalek::{Signer, SigningKey}; use sha2::{Digest, Sha256}; +use std::str::FromStr; use stellar_strkey::ed25519::PrivateKey; use soroban_env_host::xdr::{ - Asset, ContractIdPreimage, DecoratedSignature, Error as XdrError, Hash, HashIdPreimage, - HashIdPreimageContractId, Limits, Signature, SignatureHint, Transaction, TransactionEnvelope, - TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, - TransactionV1Envelope, WriteXdr, + AccountEntry, AccountEntryExt, AccountId, Asset, ContractIdPreimage, DecoratedSignature, + Error as XdrError, Hash, HashIdPreimage, HashIdPreimageContractId, Limits, PublicKey, + SequenceNumber, Signature, SignatureHint, String32, StringM, Thresholds, Transaction, + TransactionEnvelope, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, Uint256, WriteXdr, }; -pub use soroban_spec_tools::contract as contract_spec; - /// # Errors /// /// Might return an error @@ -129,6 +130,45 @@ pub fn contract_id_hash_from_asset( Ok(Hash(Sha256::digest(preimage_xdr).into())) } +const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); + +pub fn default_account_entry() -> AccountEntry { + AccountEntry { + account_id: DEFAULT_ACCOUNT_ID, + balance: 0, + seq_num: SequenceNumber(0), + num_sub_entries: 0, + inflation_dest: None, + flags: 0, + home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }), + thresholds: Thresholds([0; 4]), + signers: unsafe { [].try_into().unwrap_unchecked() }, + ext: AccountEntryExt::V0, + } +} + +/// # Errors +/// Error retrieving rpc client from url +pub async fn get_account_details( + is_view: bool, + client: &rpc::Client, + network_passphrase: &str, + key: &SigningKey, +) -> Result { + if is_view { + Ok(default_account_entry()) + } else { + client + .verify_network_passphrase(Some(network_passphrase)) + .await?; + // Get the account sequence number + let public_strkey = + stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); + let account_details = client.get_account(&public_strkey).await?; + Ok(account_details) + } +} + pub mod parsing { use regex::Regex; From 13ed0f2fc8a497940b67022e9929e94cf22dea61 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:39:46 +1000 Subject: [PATCH 08/11] Help tweaks (#1474) --- FULL_HELP_DOCS.md | 416 +++++++++--------- .../src/commands/config/locator.rs | 2 +- cmd/soroban-cli/src/commands/mod.rs | 44 +- 3 files changed, 236 insertions(+), 226 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 167549cf1..f443311ca 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -4,13 +4,15 @@ This document contains the help content for the `stellar` command-line program. ## `stellar` -With the Stellar CLI you can: +Work seamlessly with Stellar accounts, contracts, and assets from the command line. -- build, deploy and interact with contracts -- set identities to sign with -- configure networks -- generate keys -- more! +- Generate and manage keys and accounts +- Build, deploy, and interact with contracts +- Deploy asset contracts +- Stream events +- Start local testnets +- Decode, encode XDR +- More! For additional information see: @@ -18,21 +20,21 @@ For additional information see: - Smart Contract Docs: https://developers.stellar.org/docs/build/smart-contracts/overview - CLI Docs: https://developers.stellar.org/docs/tools/stellar-cli -The easiest way to get started is to generate a new identity: +To get started generate a new identity: stellar keys generate alice -You can use identities with the `--source` flag in other commands later. +Use keys with the `--source` flag in other commands. -Commands that relate to smart contract interactions are organized under the `contract` subcommand. List them: +Commands that work with contracts are organized under the `contract` subcommand. List them: stellar contract --help -A Soroban contract has its interface schema types embedded in the binary that gets deployed on-chain, making it possible to dynamically generate a custom CLI for each. The invoke subcommand makes use of this: +Use contracts like a CLI: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- --help -Anything after the `--` double dash (the "slop") is parsed as arguments to the contract-specific CLI, generated on-the-fly from the embedded schema. For the hello world example, with a function called `hello` that takes one string argument `to`, here's how you invoke it: +Anything after the `--` double dash (the "slop") is parsed as arguments to the contract-specific CLI, generated on-the-fly from the contract schema. For the hello world example, with a function called `hello` that takes one string argument `to`, here's how you invoke it: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- hello --to world @@ -41,15 +43,15 @@ Anything after the `--` double dash (the "slop") is parsed as arguments to the c ###### **Subcommands:** -* `completion` — Print shell completion code for the specified shell * `contract` — Tools for smart contract developers * `events` — Watch the network for contract events * `keys` — Create and manage identities including keys and addresses -* `xdr` — Decode and encode XDR * `network` — Start and configure networks -* `version` — Print version information * `tx` — Sign, Simulate, and Send transactions +* `xdr` — Decode and encode XDR +* `completion` — Print shell completion code for the specified shell * `cache` — Cache for transactions and contract specs +* `version` — Print version information ###### **Options:** @@ -64,28 +66,6 @@ Anything after the `--` double dash (the "slop") is parsed as arguments to the c -## `stellar completion` - -Print shell completion code for the specified shell - -Ensure the completion package for your shell is installed, e.g. bash-completion for bash. - -To enable autocomplete in the current bash shell, run: `source <(stellar completion --shell bash)` - -To enable autocomplete permanently, run: `echo "source <(stellar completion --shell bash)" >> ~/.bashrc` - - -**Usage:** `stellar completion --shell ` - -###### **Options:** - -* `--shell ` — The shell type - - Possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh` - - - - ## `stellar contract` Tools for smart contract developers @@ -819,169 +799,6 @@ Given an identity return its private key -## `stellar xdr` - -Decode and encode XDR - -**Usage:** `stellar xdr [CHANNEL] ` - -###### **Subcommands:** - -* `types` — View information about types -* `guess` — Guess the XDR type -* `decode` — Decode XDR -* `encode` — Encode XDR -* `version` — Print version information - -###### **Arguments:** - -* `` — Channel of XDR to operate on - - Default value: `+curr` - - Possible values: `+curr`, `+next` - - - - -## `stellar xdr types` - -View information about types - -**Usage:** `stellar xdr types ` - -###### **Subcommands:** - -* `list` — -* `schema` — - - - -## `stellar xdr types list` - -**Usage:** `stellar xdr types list [OPTIONS]` - -###### **Options:** - -* `--output ` - - Default value: `plain` - - Possible values: `plain`, `json`, `json-formatted` - - - - -## `stellar xdr types schema` - -**Usage:** `stellar xdr types schema [OPTIONS] --type ` - -###### **Options:** - -* `--type ` — XDR type to decode -* `--output ` - - Default value: `json-schema-draft201909` - - Possible values: `json-schema-draft7`, `json-schema-draft201909` - - - - -## `stellar xdr guess` - -Guess the XDR type - -**Usage:** `stellar xdr guess [OPTIONS] [FILE]` - -###### **Arguments:** - -* `` — File to decode, or stdin if omitted - -###### **Options:** - -* `--input ` - - Default value: `single-base64` - - Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` - -* `--output ` - - Default value: `list` - - Possible values: `list` - -* `--certainty ` — Certainty as an arbitrary value - - Default value: `2` - - - -## `stellar xdr decode` - -Decode XDR - -**Usage:** `stellar xdr decode [OPTIONS] --type [FILES]...` - -###### **Arguments:** - -* `` — Files to decode, or stdin if omitted - -###### **Options:** - -* `--type ` — XDR type to decode -* `--input ` - - Default value: `stream-base64` - - Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` - -* `--output ` - - Default value: `json` - - Possible values: `json`, `json-formatted`, `rust-debug`, `rust-debug-formatted` - - - - -## `stellar xdr encode` - -Encode XDR - -**Usage:** `stellar xdr encode [OPTIONS] --type [FILES]...` - -###### **Arguments:** - -* `` — Files to encode, or stdin if omitted - -###### **Options:** - -* `--type ` — XDR type to encode -* `--input ` - - Default value: `json` - - Possible values: `json` - -* `--output ` - - Default value: `single-base64` - - Possible values: `single`, `single-base64` - - - - -## `stellar xdr version` - -Print version information - -**Usage:** `stellar xdr version` - - - ## `stellar network` Start and configure networks @@ -1181,14 +998,6 @@ Stop a network container started with `network container start` -## `stellar version` - -Print version information - -**Usage:** `stellar version` - - - ## `stellar tx` Sign, Simulate, and Send transactions @@ -1234,6 +1043,191 @@ Calculate the hash of a transaction envelope from stdin +## `stellar xdr` + +Decode and encode XDR + +**Usage:** `stellar xdr [CHANNEL] ` + +###### **Subcommands:** + +* `types` — View information about types +* `guess` — Guess the XDR type +* `decode` — Decode XDR +* `encode` — Encode XDR +* `version` — Print version information + +###### **Arguments:** + +* `` — Channel of XDR to operate on + + Default value: `+curr` + + Possible values: `+curr`, `+next` + + + + +## `stellar xdr types` + +View information about types + +**Usage:** `stellar xdr types ` + +###### **Subcommands:** + +* `list` — +* `schema` — + + + +## `stellar xdr types list` + +**Usage:** `stellar xdr types list [OPTIONS]` + +###### **Options:** + +* `--output ` + + Default value: `plain` + + Possible values: `plain`, `json`, `json-formatted` + + + + +## `stellar xdr types schema` + +**Usage:** `stellar xdr types schema [OPTIONS] --type ` + +###### **Options:** + +* `--type ` — XDR type to decode +* `--output ` + + Default value: `json-schema-draft201909` + + Possible values: `json-schema-draft7`, `json-schema-draft201909` + + + + +## `stellar xdr guess` + +Guess the XDR type + +**Usage:** `stellar xdr guess [OPTIONS] [FILE]` + +###### **Arguments:** + +* `` — File to decode, or stdin if omitted + +###### **Options:** + +* `--input ` + + Default value: `single-base64` + + Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` + +* `--output ` + + Default value: `list` + + Possible values: `list` + +* `--certainty ` — Certainty as an arbitrary value + + Default value: `2` + + + +## `stellar xdr decode` + +Decode XDR + +**Usage:** `stellar xdr decode [OPTIONS] --type [FILES]...` + +###### **Arguments:** + +* `` — Files to decode, or stdin if omitted + +###### **Options:** + +* `--type ` — XDR type to decode +* `--input ` + + Default value: `stream-base64` + + Possible values: `single`, `single-base64`, `stream`, `stream-base64`, `stream-framed` + +* `--output ` + + Default value: `json` + + Possible values: `json`, `json-formatted`, `rust-debug`, `rust-debug-formatted` + + + + +## `stellar xdr encode` + +Encode XDR + +**Usage:** `stellar xdr encode [OPTIONS] --type [FILES]...` + +###### **Arguments:** + +* `` — Files to encode, or stdin if omitted + +###### **Options:** + +* `--type ` — XDR type to encode +* `--input ` + + Default value: `json` + + Possible values: `json` + +* `--output ` + + Default value: `single-base64` + + Possible values: `single`, `single-base64` + + + + +## `stellar xdr version` + +Print version information + +**Usage:** `stellar xdr version` + + + +## `stellar completion` + +Print shell completion code for the specified shell + +Ensure the completion package for your shell is installed, e.g. bash-completion for bash. + +To enable autocomplete in the current bash shell, run: `source <(stellar completion --shell bash)` + +To enable autocomplete permanently, run: `echo "source <(stellar completion --shell bash)" >> ~/.bashrc` + + +**Usage:** `stellar completion --shell ` + +###### **Options:** + +* `--shell ` — The shell type + + Possible values: `bash`, `elvish`, `fish`, `powershell`, `zsh` + + + + ## `stellar cache` Cache for transactions and contract specs @@ -1303,3 +1297,11 @@ Read cached action +## `stellar version` + +Print version information + +**Usage:** `stellar version` + + + diff --git a/cmd/soroban-cli/src/commands/config/locator.rs b/cmd/soroban-cli/src/commands/config/locator.rs index 5dc4dae85..cbcb9e8b3 100644 --- a/cmd/soroban-cli/src/commands/config/locator.rs +++ b/cmd/soroban-cli/src/commands/config/locator.rs @@ -80,7 +80,7 @@ pub struct Args { pub global: bool, /// Location of config directory, default is "." - #[arg(long, help_heading = "TESTING_OPTIONS")] + #[arg(long)] pub config_dir: Option, } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 0d5fcff57..a9f10bdd8 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -18,13 +18,16 @@ pub mod version; pub mod txn_result; pub const HEADING_RPC: &str = "Options (RPC)"; -const ABOUT: &str = "With the Stellar CLI you can: +const ABOUT: &str = + "Work seamlessly with Stellar accounts, contracts, and assets from the command line. -- build, deploy and interact with contracts -- set identities to sign with -- configure networks -- generate keys -- more! +- Generate and manage keys and accounts +- Build, deploy, and interact with contracts +- Deploy asset contracts +- Stream events +- Start local testnets +- Decode, encode XDR +- More! For additional information see: @@ -35,21 +38,21 @@ For additional information see: // long_about is shown when someone uses `--help`; short help when using `-h` const LONG_ABOUT: &str = " -The easiest way to get started is to generate a new identity: +To get started generate a new identity: stellar keys generate alice -You can use identities with the `--source` flag in other commands later. +Use keys with the `--source` flag in other commands. -Commands that relate to smart contract interactions are organized under the `contract` subcommand. List them: +Commands that work with contracts are organized under the `contract` subcommand. List them: stellar contract --help -A Soroban contract has its interface schema types embedded in the binary that gets deployed on-chain, making it possible to dynamically generate a custom CLI for each. The invoke subcommand makes use of this: +Use contracts like a CLI: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- --help -Anything after the `--` double dash (the \"slop\") is parsed as arguments to the contract-specific CLI, generated on-the-fly from the embedded schema. For the hello world example, with a function called `hello` that takes one string argument `to`, here's how you invoke it: +Anything after the `--` double dash (the \"slop\") is parsed as arguments to the contract-specific CLI, generated on-the-fly from the contract schema. For the hello world example, with a function called `hello` that takes one string argument `to`, here's how you invoke it: stellar contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- hello --to world "; @@ -125,30 +128,35 @@ impl FromStr for Root { #[derive(Parser, Debug)] pub enum Cmd { - /// Print shell completion code for the specified shell. - #[command(long_about = completion::LONG_ABOUT)] - Completion(completion::Cmd), /// Tools for smart contract developers #[command(subcommand)] Contract(contract::Cmd), /// Watch the network for contract events Events(events::Cmd), + /// Create and manage identities including keys and addresses #[command(subcommand)] Keys(keys::Cmd), - /// Decode and encode XDR - Xdr(stellar_xdr::cli::Root), + /// Start and configure networks #[command(subcommand)] Network(network::Cmd), - /// Print version information - Version(version::Cmd), + /// Sign, Simulate, and Send transactions #[command(subcommand)] Tx(tx::Cmd), + + /// Decode and encode XDR + Xdr(stellar_xdr::cli::Root), + + /// Print shell completion code for the specified shell. + #[command(long_about = completion::LONG_ABOUT)] + Completion(completion::Cmd), /// Cache for transactions and contract specs #[command(subcommand)] Cache(cache::Cmd), + /// Print version information + Version(version::Cmd), } #[derive(thiserror::Error, Debug)] From c2b6d2f6f7259c770774721d68cd5bb8f7b64523 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 24 Jul 2024 23:54:45 +1000 Subject: [PATCH 09/11] Remove the short option -o for --overwrite (#1483) --- FULL_HELP_DOCS.md | 2 +- cmd/soroban-cli/src/commands/contract/init.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index f443311ca..6c694bce8 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -404,7 +404,7 @@ Initialize a Soroban project with an example contract * `-f`, `--frontend-template ` — An optional flag to pass in a url for a frontend template repository. Default value: `` -* `-o`, `--overwrite` — Overwrite all existing files. +* `--overwrite` — Overwrite all existing files. diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index 26bf5816a..7318c1d7f 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -48,7 +48,7 @@ pub struct Cmd { )] pub frontend_template: String, - #[arg(short, long, long_help = "Overwrite all existing files.")] + #[arg(long, long_help = "Overwrite all existing files.")] pub overwrite: bool, } From 91f95de3e6cd0af01217c77ce6780a29279a66fa Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 25 Jul 2024 22:22:41 +1000 Subject: [PATCH 10/11] Revert "change fund to top off if balance below 10k xlm (#1478)" (#1487) --- Cargo.lock | 248 ------------------ .../tests/it/integration/hello_world.rs | 9 +- cmd/soroban-cli/Cargo.toml | 1 - .../src/commands/contract/deploy/asset.rs | 12 +- .../src/commands/contract/invoke.rs | 48 +++- cmd/soroban-cli/src/commands/keys/fund.rs | 187 +------------ cmd/soroban-cli/src/fee.rs | 14 +- cmd/soroban-cli/src/utils.rs | 54 +--- 8 files changed, 62 insertions(+), 511 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eff278396..edab8bb9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -128,12 +117,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - [[package]] name = "ascii-canvas" version = "3.0.0" @@ -502,18 +485,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -589,30 +560,6 @@ dependencies = [ "serde_with", ] -[[package]] -name = "borsh" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.39", - "syn_derive", -] - [[package]] name = "bstr" version = "1.9.1" @@ -639,28 +586,6 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -734,12 +659,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.38" @@ -1584,12 +1503,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" version = "0.3.30" @@ -2568,9 +2481,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "hashbrown" @@ -3883,38 +3793,6 @@ dependencies = [ "elliptic-curve", ] -[[package]] -name = "proc-macro-crate" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" -dependencies = [ - "toml_edit 0.21.1", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.69" @@ -3930,26 +3808,6 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "quote" version = "1.0.33" @@ -3959,12 +3817,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -4074,15 +3926,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - [[package]] name = "reqwest" version = "0.11.27" @@ -4152,35 +3995,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rpassword" version = "7.3.1" @@ -4236,22 +4050,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust_decimal" -version = "1.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" -dependencies = [ - "arrayvec", - "borsh", - "bytes", - "num-traits", - "rand", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4465,12 +4263,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "sec1" version = "0.7.2" @@ -4764,12 +4556,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - [[package]] name = "similar" version = "2.5.0" @@ -4914,7 +4700,6 @@ dependencies = [ "regex", "rpassword", "rust-embed", - "rust_decimal", "sep5", "serde", "serde-aux", @@ -5434,18 +5219,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.39", -] - [[package]] name = "sync_wrapper" version = "0.1.2" @@ -5473,12 +5246,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "temp-dir" version = "0.1.13" @@ -6067,12 +5834,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" - [[package]] name = "valuable" version = "0.1.0" @@ -6544,15 +6305,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index b6b6f5379..ec0c843db 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -35,7 +35,7 @@ async fn invoke() { .arg("fund") .arg("test") .assert() - .stdout(predicates::str::contains("Nothing to do.")); + .stderr(predicates::str::contains("Account already exists")); sandbox .new_assert_cmd("keys") .arg("fund") @@ -138,13 +138,6 @@ async fn invoke() { handles_kebab_case(sandbox, id).await; fetch(sandbox, id).await; invoke_prng_u64_in_range_test(sandbox, id).await; - // test fund will add when account exists - sandbox - .new_assert_cmd("keys") - .arg("fund") - .arg("test") - .assert() - .stdout(predicates::str::contains("New test balance:")); } fn invoke_hello_world(sandbox: &TestEnv, id: &str) { diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 0fdfa7494..6d40ddd90 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -100,7 +100,6 @@ pathdiff = "0.2.1" dotenvy = "0.15.7" directories = { workspace = true } ulid = { workspace = true, features = ["serde"] } -rust_decimal = "1.35.0" strum = "0.17.1" strum_macros = "0.17.1" gix = { version = "0.58.0", default-features = false, features = [ diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 4d770d107..e912d2b98 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -19,7 +19,7 @@ use crate::{ NetworkRunnable, }, rpc::{Client, Error as SorobanRpcError}, - utils::{contract_id_hash_from_asset, get_account_details, parsing::parse_asset}, + utils::{contract_id_hash_from_asset, parsing::parse_asset}, }; #[derive(thiserror::Error, Debug)] @@ -94,10 +94,16 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; let key = config.key_pair()?; + + // Get the account sequence number + let public_strkey = + stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); // TODO: use symbols for the method names (both here and in serve) - let account_details = - get_account_details(false, &client, &network.network_passphrase, &key).await?; + let account_details = client.get_account(&public_strkey).await?; let sequence: i64 = account_details.seq_num.into(); let network_passphrase = &network.network_passphrase; let contract_id = contract_id_hash_from_asset(&asset, network_passphrase)?; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 74be03c0d..22462cbcb 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -12,10 +12,11 @@ use heck::ToKebabCase; use soroban_env_host::{ xdr::{ - self, AccountId, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, - LedgerEntryData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, - PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, - SequenceNumber, Transaction, TransactionExt, Uint256, VecM, WriteXdr, + self, AccountEntry, AccountEntryExt, AccountId, Hash, HostFunction, InvokeContractArgs, + InvokeHostFunctionOp, LedgerEntryData, Limits, Memo, MuxedAccount, Operation, + OperationBody, Preconditions, PublicKey, ScAddress, ScSpecEntry, ScSpecFunctionV0, + ScSpecTypeDef, ScVal, ScVec, SequenceNumber, String32, StringM, Thresholds, Transaction, + TransactionExt, Uint256, VecM, WriteXdr, }, HostError, }; @@ -29,7 +30,6 @@ use super::super::{ use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; -use crate::utils::get_account_details; use crate::{ commands::{config::data, global, network}, rpc, Pwd, @@ -321,14 +321,19 @@ impl NetworkRunnable for Cmd { let _ = self.build_host_function_parameters(contract_id, spec_entries, config)?; } let client = rpc::Client::new(&network.rpc_url)?; - let key = config.key_pair()?; - let account_details = get_account_details( - self.is_view, - &client.clone(), - &network.network_passphrase, - &key, - ) - .await?; + let account_details = if self.is_view { + default_account_entry() + } else { + client + .verify_network_passphrase(Some(&network.network_passphrase)) + .await?; + let key = config.key_pair()?; + + // Get the account sequence number + let public_strkey = + stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); + client.get_account(&public_strkey).await? + }; let sequence: i64 = account_details.seq_num.into(); let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id; @@ -391,6 +396,8 @@ impl NetworkRunnable for Cmd { } } +const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); + // fn log_auth_cost_and_footprint(resources: Option<&SorobanResources>) { // if let Some(resources) = resources { // crate::log::footprint(&resources.footprint); @@ -419,6 +426,21 @@ impl NetworkRunnable for Cmd { // .unwrap_or_default() // } +fn default_account_entry() -> AccountEntry { + AccountEntry { + account_id: DEFAULT_ACCOUNT_ID, + balance: 0, + seq_num: SequenceNumber(0), + num_sub_entries: 0, + inflation_dest: None, + flags: 0, + home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }), + thresholds: Thresholds([0; 4]), + signers: unsafe { [].try_into().unwrap_unchecked() }, + ext: AccountEntryExt::V0, + } +} + pub fn output_to_string( spec: &Spec, res: &ScVal, diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index 5135d21bb..b6c088f13 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -1,18 +1,7 @@ use clap::command; -use rand::{thread_rng, Rng}; -use rust_decimal::Decimal; -use soroban_sdk::xdr; -use stellar_strkey::ed25519::{PrivateKey, PublicKey}; -use crate::commands::config::secret::Secret; -use crate::commands::global; -use crate::commands::network::LOCAL_NETWORK_PASSPHRASE; -use crate::utils::contract_id_hash_from_asset; -use crate::utils::parsing::{self, parse_asset}; -use crate::{commands, rpc, CommandParser}; -use crate::{commands::network, utils::get_account_details}; +use crate::commands::network; -use super::super::config::secret; use super::address; #[derive(thiserror::Error, Debug)] @@ -21,26 +10,6 @@ pub enum Error { Address(#[from] address::Error), #[error(transparent)] Network(#[from] network::Error), - #[error(transparent)] - Secret(#[from] secret::Error), - #[error(transparent)] - Parsing(#[from] parsing::Error), - #[error(transparent)] - Rpc(#[from] rpc::Error), - #[error(transparent)] - Xdr(#[from] xdr::Error), - #[error(transparent)] - Clap(#[from] clap::Error), - #[error("RPC URL is missing in the network configuration")] - MissingRpcUrl, - #[error("Network passphrase is missing in the network configuration")] - MissingNetworkPassphrase, - #[error("Asset contract could not be deployed")] - AssetContractError, - #[error("Failed to transfer funds: {0}")] - FundTransferError(String), - #[error("Problem deploying asset contract: {0}")] - AssetDeploymentError(String), } #[derive(Debug, clap::Parser, Clone)] @@ -53,159 +22,13 @@ pub struct Cmd { pub address: address::Cmd, } -const STROOPS_PER_XLM: i64 = 10_000_000; -const DEFAULT_FRIENDBOT_AMOUNT: i64 = 10_000 * STROOPS_PER_XLM; - -fn convert_xlm_rounded(balance: i64) -> Decimal { - let xlm_balance = Decimal::new(balance, 0) / Decimal::new(STROOPS_PER_XLM, 0); - xlm_balance.round_dp(2) -} - impl Cmd { - pub async fn check_balance(&self) -> Result { - let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; - let network_passphrase = self - .network - .network_passphrase - .as_ref() - .ok_or(Error::MissingNetworkPassphrase)?; - let client = rpc::Client::new(rpc_url)?; - let key = self.address.private_key()?; - let account_details = - get_account_details(false, &client.clone(), network_passphrase, &key).await?; - Ok(account_details.balance) - } - - pub async fn create_temp_account(&self) -> Result<(PublicKey, PrivateKey), Error> { - let random_seed: String = thread_rng() - .sample_iter(&rand::distributions::Alphanumeric) - .take(16) - .map(char::from) - .collect(); - let secret = Secret::from_seed(Some(&random_seed))?; - let addr = secret.public_key(self.address.hd_path)?; - let private_key = secret.private_key(self.address.hd_path)?; - let network = self.network.get(&self.address.locator)?; - network - .fund_address(&addr) - .await - .map_err(|e| { - tracing::warn!("fund_address failed: {e}"); - }) - .unwrap_or_default(); - Ok((addr, private_key)) - } - - pub async fn get_asset_contract_id(&self) -> Result { - // if network is local, deploy the Stellar Asset contract and retrieve ID - let network = self.network.get(&self.address.locator)?; - let asset = parse_asset("native")?; - let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; - if network.network_passphrase == LOCAL_NETWORK_PASSPHRASE { - let cmd = commands::contract::deploy::asset::Cmd::parse_arg_vec(&[ - "--asset", - "native", - "--source-account", - &self.address.name, - "--rpc-url", - rpc_url, - "--network-passphrase", - &network.network_passphrase, - ])?; - match cmd.run().await { - Ok(()) => { - let contract_id = - contract_id_hash_from_asset(&asset, &network.network_passphrase)?; - Ok(stellar_strkey::Contract(contract_id.0)) - } - Err(err) => { - if err.to_string().contains("ExistingValue") { - let contract_id = - contract_id_hash_from_asset(&asset, &network.network_passphrase)?; - Ok(stellar_strkey::Contract(contract_id.0)) - } else { - Err(Error::AssetDeploymentError(err.to_string())) - } - } - } - } else { - let contract_id = contract_id_hash_from_asset(&asset, &network.network_passphrase)?; - Ok(stellar_strkey::Contract(contract_id.0)) - } - } - - pub async fn add_funds(&self, amount: i64) -> Result<(), Error> { - let id = self.get_asset_contract_id().await?; - let addr = self.address.public_key()?; - let rpc_url = self.network.rpc_url.as_ref().ok_or(Error::MissingRpcUrl)?; - let network_passphrase = self - .network - .network_passphrase - .as_ref() - .ok_or(Error::MissingNetworkPassphrase)?; - let (from_id, temp_secret) = self.create_temp_account().await?; - let cmd = commands::contract::invoke::Cmd::parse_arg_vec(&[ - "--id", - &id.to_string(), - "--source-account", - &temp_secret.to_string(), - "--rpc-url", - rpc_url, - "--network-passphrase", - network_passphrase, - "--", - "transfer", - "--to", - &addr.to_string(), - "--from", - &from_id.to_string(), - "--amount", - &amount.to_string(), - ])?; - cmd.run(&global::Args { - locator: self.address.locator.clone(), - ..Default::default() - }) - .await - .map_err(|e| Error::FundTransferError(e.to_string())) - } - pub async fn run(&self) -> Result<(), Error> { let addr = self.address.public_key()?; - let balance = self.check_balance().await.unwrap_or_else(|err| { - eprintln!("Failed to check balance: {err}"); - 0 - }); - let rounded_xlm = convert_xlm_rounded(balance); - if balance >= DEFAULT_FRIENDBOT_AMOUNT { - println!( - "Current {} balance: {} XLM. Nothing to do.", - self.address.name, rounded_xlm, - ); - } else if balance == 0 { - self.network - .get(&self.address.locator)? - .fund_address(&addr) - .await?; - } else { - println!( - "Current {} balance: {} XLM. Topping off...", - self.address.name, rounded_xlm, - ); - self.add_funds(DEFAULT_FRIENDBOT_AMOUNT - balance).await?; - let balance = match self.check_balance().await { - Ok(balance) => balance, - Err(err) => { - eprintln!("Failed to check balance: {err}"); - 0 - } - }; - println!( - "New {} balance: {} XLM.", - self.address.name, - convert_xlm_rounded(balance), - ); - } + self.network + .get(&self.address.locator)? + .fund_address(&addr) + .await?; Ok(()) } } diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 0e7ccf580..698d66007 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -27,15 +27,11 @@ pub struct Args { impl Args { pub fn apply_to_assembled_txn(&self, txn: Assembled) -> Assembled { - apply_max_instructions_to_txn(txn, self.instructions) - } -} - -pub fn apply_max_instructions_to_txn(txn: Assembled, max_instructions: Option) -> Assembled { - if let Some(instructions) = max_instructions { - txn.set_max_instructions(instructions) - } else { - add_padding_to_instructions(txn) + if let Some(instructions) = self.instructions { + txn.set_max_instructions(instructions) + } else { + add_padding_to_instructions(txn) + } } } diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 4ace5c906..e74222c2c 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,17 +1,16 @@ -use crate::rpc; -use ed25519_dalek::{Signer, SigningKey}; +use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; -use std::str::FromStr; use stellar_strkey::ed25519::PrivateKey; use soroban_env_host::xdr::{ - AccountEntry, AccountEntryExt, AccountId, Asset, ContractIdPreimage, DecoratedSignature, - Error as XdrError, Hash, HashIdPreimage, HashIdPreimageContractId, Limits, PublicKey, - SequenceNumber, Signature, SignatureHint, String32, StringM, Thresholds, Transaction, - TransactionEnvelope, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, - TransactionV1Envelope, Uint256, WriteXdr, + Asset, ContractIdPreimage, DecoratedSignature, Error as XdrError, Hash, HashIdPreimage, + HashIdPreimageContractId, Limits, Signature, SignatureHint, Transaction, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, WriteXdr, }; +pub use soroban_spec_tools::contract as contract_spec; + /// # Errors /// /// Might return an error @@ -130,45 +129,6 @@ pub fn contract_id_hash_from_asset( Ok(Hash(Sha256::digest(preimage_xdr).into())) } -const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32]))); - -pub fn default_account_entry() -> AccountEntry { - AccountEntry { - account_id: DEFAULT_ACCOUNT_ID, - balance: 0, - seq_num: SequenceNumber(0), - num_sub_entries: 0, - inflation_dest: None, - flags: 0, - home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }), - thresholds: Thresholds([0; 4]), - signers: unsafe { [].try_into().unwrap_unchecked() }, - ext: AccountEntryExt::V0, - } -} - -/// # Errors -/// Error retrieving rpc client from url -pub async fn get_account_details( - is_view: bool, - client: &rpc::Client, - network_passphrase: &str, - key: &SigningKey, -) -> Result { - if is_view { - Ok(default_account_entry()) - } else { - client - .verify_network_passphrase(Some(network_passphrase)) - .await?; - // Get the account sequence number - let public_strkey = - stellar_strkey::ed25519::PublicKey(key.verifying_key().to_bytes()).to_string(); - let account_details = client.get_account(&public_strkey).await?; - Ok(account_details) - } -} - pub mod parsing { use regex::Regex; From d0c077bdad427cc85a098daec4867350a7e4ea35 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 25 Jul 2024 14:21:49 -0400 Subject: [PATCH 11/11] chore: reorganize config into separate module (#1479) --- cmd/crates/soroban-test/src/lib.rs | 3 +- cmd/crates/soroban-test/tests/it/config.rs | 3 +- .../tests/it/integration/hello_world.rs | 8 +- cmd/crates/soroban-test/tests/it/util.rs | 4 +- .../src/commands/cache/actionlog/ls.rs | 3 +- .../src/commands/cache/actionlog/read.rs | 3 +- cmd/soroban-cli/src/commands/cache/clean.rs | 3 +- cmd/soroban-cli/src/commands/cache/path.rs | 3 +- .../commands/contract/bindings/typescript.rs | 8 +- .../src/commands/contract/deploy/asset.rs | 4 +- .../src/commands/contract/deploy/wasm.rs | 6 +- .../src/commands/contract/extend.rs | 4 +- .../src/commands/contract/fetch.rs | 6 +- .../src/commands/contract/id/asset.rs | 4 +- .../src/commands/contract/id/wasm.rs | 4 +- .../src/commands/contract/inspect.rs | 2 +- .../src/commands/contract/install.rs | 6 +- .../src/commands/contract/invoke.rs | 8 +- cmd/soroban-cli/src/commands/contract/read.rs | 6 +- .../src/commands/contract/restore.rs | 4 +- cmd/soroban-cli/src/commands/events.rs | 6 +- cmd/soroban-cli/src/commands/keys/add.rs | 2 +- cmd/soroban-cli/src/commands/keys/fund.rs | 2 +- cmd/soroban-cli/src/commands/keys/generate.rs | 4 +- cmd/soroban-cli/src/commands/keys/show.rs | 2 +- cmd/soroban-cli/src/commands/mod.rs | 3 +- cmd/soroban-cli/src/commands/network/add.rs | 4 +- cmd/soroban-cli/src/commands/network/mod.rs | 177 +--------------- cmd/soroban-cli/src/commands/tx/hash.rs | 7 +- .../src/{commands => }/config/alias.rs | 0 .../src/{commands => }/config/data.rs | 0 .../src/{commands => }/config/locator.rs | 2 +- .../src/{commands => }/config/mod.rs | 5 +- cmd/soroban-cli/src/config/network.rs | 200 ++++++++++++++++++ .../src/config/network/passphrase.rs | 4 + .../src/{commands => }/config/secret.rs | 0 cmd/soroban-cli/src/get_spec.rs | 5 +- cmd/soroban-cli/src/lib.rs | 1 + 38 files changed, 269 insertions(+), 247 deletions(-) rename cmd/soroban-cli/src/{commands => }/config/alias.rs (100%) rename cmd/soroban-cli/src/{commands => }/config/data.rs (100%) rename cmd/soroban-cli/src/{commands => }/config/locator.rs (99%) rename cmd/soroban-cli/src/{commands => }/config/mod.rs (97%) create mode 100644 cmd/soroban-cli/src/config/network.rs create mode 100644 cmd/soroban-cli/src/config/network/passphrase.rs rename cmd/soroban-cli/src/{commands => }/config/secret.rs (100%) diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 8df6a97c0..544e2d59e 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -30,7 +30,8 @@ use assert_fs::{fixture::FixtureError, prelude::PathChild, TempDir}; use fs_extra::dir::CopyOptions; use soroban_cli::{ - commands::{config, contract::invoke, global, keys, network, NetworkRunnable}, + commands::{contract::invoke, global, keys, NetworkRunnable}, + config::{self, network}, CommandParser, }; diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 5704237ed..70dfaa693 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -3,7 +3,8 @@ use soroban_test::{AssertExt, TestEnv}; use std::{fs, path::Path}; use crate::util::{add_key, add_test_id, SecretKind, DEFAULT_SEED_PHRASE}; -use soroban_cli::commands::network::{self, LOCAL_NETWORK_PASSPHRASE}; +use soroban_cli::commands::network; +use soroban_cli::config::network::passphrase::LOCAL as LOCAL_NETWORK_PASSPHRASE; fn ls(sandbox: &TestEnv) -> Vec { sandbox diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index ec0c843db..1c766095f 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -1,8 +1,10 @@ use predicates::boolean::PredicateBooleanExt; -use soroban_cli::commands::{ +use soroban_cli::{ + commands::{ + contract::{self, fetch}, + txn_result::TxnResult, + }, config::{locator, secret}, - contract::{self, fetch}, - txn_result::TxnResult, }; use soroban_rpc::GetLatestLedgerResponse; use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index 6bf5f8c22..f424ea1ae 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -1,8 +1,8 @@ use std::path::Path; -use soroban_cli::commands::{ +use soroban_cli::{ + commands::contract, config::{locator::KeyType, secret::Secret}, - contract, }; use soroban_test::{TestEnv, Wasm, TEST_ACCOUNT}; diff --git a/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs b/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs index cb7a958c6..360512bba 100644 --- a/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs +++ b/cmd/soroban-cli/src/commands/cache/actionlog/ls.rs @@ -1,7 +1,6 @@ use clap::command; -use super::super::super::config::locator; -use crate::commands::config::data; +use crate::config::{data, locator}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/cache/actionlog/read.rs b/cmd/soroban-cli/src/commands/cache/actionlog/read.rs index 67b4358f1..4991ac6ef 100644 --- a/cmd/soroban-cli/src/commands/cache/actionlog/read.rs +++ b/cmd/soroban-cli/src/commands/cache/actionlog/read.rs @@ -1,7 +1,6 @@ use std::{fs, io, path::PathBuf}; -use super::super::super::config::locator; -use crate::commands::config::data; +use crate::config::{data, locator}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/cache/clean.rs b/cmd/soroban-cli/src/commands/cache/clean.rs index bea0a43d4..e2378aa6c 100644 --- a/cmd/soroban-cli/src/commands/cache/clean.rs +++ b/cmd/soroban-cli/src/commands/cache/clean.rs @@ -1,7 +1,6 @@ use std::{fs, io::ErrorKind}; -use super::super::config::locator; -use crate::commands::config::data; +use crate::config::{data, locator}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/cache/path.rs b/cmd/soroban-cli/src/commands/cache/path.rs index 337608735..a382516b9 100644 --- a/cmd/soroban-cli/src/commands/cache/path.rs +++ b/cmd/soroban-cli/src/commands/cache/path.rs @@ -1,5 +1,4 @@ -use super::super::config::locator; -use crate::commands::config::data; +use crate::config::{data, locator}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index b2b6c3b1c..84e2b1762 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -7,12 +7,10 @@ use stellar_strkey::DecodeError; use crate::wasm; use crate::{ - commands::{ - config::{self, locator}, - contract::fetch, - global, + commands::{contract::fetch, global, NetworkRunnable}, + config::{ + self, locator, network::{self, Network}, - NetworkRunnable, }, get_spec::{self, get_remote_contract_spec}, }; diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index e912d2b98..98276a86d 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -13,11 +13,11 @@ use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ commands::{ - config::{self, data}, - global, network, + global, txn_result::{TxnEnvelopeResult, TxnResult}, NetworkRunnable, }, + config::{self, data, network}, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, }; diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index b85b65bcc..4c1aa5d55 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -16,14 +16,14 @@ use soroban_env_host::{ }; use crate::commands::{ - config::{data, locator}, contract::{self, id::wasm::get_contract_id}, - global, network, + global, txn_result::{TxnEnvelopeResult, TxnResult}, NetworkRunnable, }; use crate::{ - commands::{config, contract::install, HEADING_RPC}, + commands::{contract::install, HEADING_RPC}, + config::{self, data, locator, network}, rpc::{self, Client}, utils, wasm, }; diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 89bdc7620..03c7c70cf 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -10,11 +10,11 @@ use soroban_env_host::xdr::{ use crate::{ commands::{ - config::{self, data, locator}, - global, network, + global, txn_result::{TxnEnvelopeResult, TxnResult}, NetworkRunnable, }, + config::{self, data, locator, network}, key, rpc::{self, Client}, wasm, Pwd, diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index 42f474558..7c6c84cb1 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -19,9 +19,11 @@ use soroban_env_host::{ use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; -use super::super::config::{self, locator}; -use crate::commands::network::{self, Network}; use crate::commands::{global, NetworkRunnable}; +use crate::config::{ + self, locator, + network::{self, Network}, +}; use crate::{ rpc::{self, Client}, Pwd, diff --git a/cmd/soroban-cli/src/commands/contract/id/asset.rs b/cmd/soroban-cli/src/commands/contract/id/asset.rs index e036b7939..31b55f8d1 100644 --- a/cmd/soroban-cli/src/commands/contract/id/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/id/asset.rs @@ -1,6 +1,6 @@ use clap::{arg, command, Parser}; -use crate::commands::config; +use crate::config; use crate::utils::contract_id_hash_from_asset; use crate::utils::parsing::parse_asset; @@ -20,7 +20,7 @@ pub enum Error { #[error(transparent)] ParseError(#[from] crate::utils::parsing::Error), #[error(transparent)] - ConfigError(#[from] crate::commands::config::Error), + ConfigError(#[from] config::Error), #[error(transparent)] Xdr(#[from] soroban_env_host::xdr::Error), } diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 9c02f07d2..14824b145 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -5,7 +5,7 @@ use soroban_env_host::xdr::{ HashIdPreimageContractId, Limits, PublicKey, ScAddress, Uint256, WriteXdr, }; -use crate::commands::config; +use crate::config; #[derive(Parser, Debug, Clone)] #[group(skip)] @@ -22,7 +22,7 @@ pub enum Error { #[error(transparent)] ParseError(#[from] crate::utils::parsing::Error), #[error(transparent)] - ConfigError(#[from] crate::commands::config::Error), + ConfigError(#[from] config::Error), #[error(transparent)] Xdr(#[from] xdr::Error), #[error("cannot parse salt {0}")] diff --git a/cmd/soroban-cli/src/commands/contract/inspect.rs b/cmd/soroban-cli/src/commands/contract/inspect.rs index e66bf83fd..36a1a2302 100644 --- a/cmd/soroban-cli/src/commands/contract/inspect.rs +++ b/cmd/soroban-cli/src/commands/contract/inspect.rs @@ -5,7 +5,7 @@ use std::{fmt::Debug, path::PathBuf}; use tracing::debug; use super::SpecOutput; -use crate::{commands::config::locator, wasm}; +use crate::{config::locator, wasm}; #[derive(Parser, Debug, Clone)] #[group(skip)] diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index d7ebef16d..9d2e474a3 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -11,12 +11,12 @@ use soroban_env_host::xdr::{ }; use super::restore; -use crate::commands::network; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; -use crate::commands::{config::data, global, NetworkRunnable}; +use crate::commands::{global, NetworkRunnable}; +use crate::config::{self, data, network}; use crate::key; use crate::rpc::{self, Client}; -use crate::{commands::config, utils, wasm}; +use crate::{utils, wasm}; const CONTRACT_META_SDK_KEY: &str = "rssdkver"; const PUBLIC_NETWORK_PASSPHRASE: &str = "Public Global Stellar Network ; September 2015"; diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 22462cbcb..041613ba2 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -23,15 +23,13 @@ use soroban_env_host::{ use soroban_spec::read::FromWasmError; -use super::super::{ - config::{self, locator}, - events, -}; +use super::super::events; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; use crate::get_spec::{self, get_remote_contract_spec}; use crate::{ - commands::{config::data, global, network}, + commands::global, + config::{self, data, locator, network}, rpc, Pwd, }; use soroban_spec_tools::{contract, Spec}; diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 70e2bd09e..bef3f3737 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -13,10 +13,8 @@ use soroban_env_host::{ }; use crate::{ - commands::{ - config::{self, locator}, - global, NetworkRunnable, - }, + commands::{global, NetworkRunnable}, + config::{self, locator}, key, rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 2a6669920..192af3140 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -11,12 +11,12 @@ use stellar_strkey::DecodeError; use crate::{ commands::{ - config::{self, data, locator}, contract::extend, - global, network, + global, txn_result::{TxnEnvelopeResult, TxnResult}, NetworkRunnable, }, + config::{self, data, locator, network}, key, rpc::{self, Client}, wasm, Pwd, diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index e9f3ba645..a755c33d0 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -3,10 +3,8 @@ use std::io; use soroban_env_host::xdr::{self, Limits, ReadXdr}; -use super::{ - config::{self, locator}, - global, network, NetworkRunnable, -}; +use super::{global, NetworkRunnable}; +use crate::config::{self, locator, network}; use crate::rpc; #[derive(Parser, Debug, Clone)] diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index 2868c7371..d8f528bae 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -1,6 +1,6 @@ use clap::command; -use super::super::config::{locator, secret}; +use crate::config::{locator, secret}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/keys/fund.rs b/cmd/soroban-cli/src/commands/keys/fund.rs index b6c088f13..d7100c6cb 100644 --- a/cmd/soroban-cli/src/commands/keys/fund.rs +++ b/cmd/soroban-cli/src/commands/keys/fund.rs @@ -1,6 +1,6 @@ use clap::command; -use crate::commands::network; +use crate::config::network; use super::address; diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 5c6c119ca..16e6a7fdb 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -1,9 +1,7 @@ use clap::{arg, command}; -use crate::commands::network; - use super::super::config::{ - locator, + locator, network, secret::{self, Secret}, }; diff --git a/cmd/soroban-cli/src/commands/keys/show.rs b/cmd/soroban-cli/src/commands/keys/show.rs index b99478cbc..58c47740c 100644 --- a/cmd/soroban-cli/src/commands/keys/show.rs +++ b/cmd/soroban-cli/src/commands/keys/show.rs @@ -1,6 +1,6 @@ use clap::arg; -use super::super::config::{locator, secret}; +use crate::config::{locator, secret}; #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index a9f10bdd8..081439254 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -3,9 +3,10 @@ use std::str::FromStr; use async_trait::async_trait; use clap::{command, error::ErrorKind, CommandFactory, FromArgMatches, Parser}; +use crate::config; + pub mod cache; pub mod completion; -pub mod config; pub mod contract; pub mod events; pub mod global; diff --git a/cmd/soroban-cli/src/commands/network/add.rs b/cmd/soroban-cli/src/commands/network/add.rs index b6a2ddd38..20b1afa7b 100644 --- a/cmd/soroban-cli/src/commands/network/add.rs +++ b/cmd/soroban-cli/src/commands/network/add.rs @@ -1,4 +1,4 @@ -use super::super::config::{locator, secret}; +use crate::config::{locator, network, secret}; use clap::command; #[derive(thiserror::Error, Debug)] @@ -17,7 +17,7 @@ pub struct Cmd { pub name: String, #[command(flatten)] - pub network: super::Network, + pub network: network::Network, #[command(flatten)] pub config_locator: locator::Args, diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 1c34f92c8..d0b4f32a3 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -1,20 +1,9 @@ -use std::str::FromStr; +use clap::Parser; -use clap::{arg, Parser}; -use phf::phf_map; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use stellar_strkey::ed25519::PublicKey; - -use crate::{ - commands::HEADING_RPC, - rpc::{self, Client}, -}; +use crate::rpc::{self}; use super::config::locator; -pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017"; - pub mod add; pub mod container; pub mod ls; @@ -115,165 +104,3 @@ impl Cmd { Ok(()) } } - -#[derive(Debug, clap::Args, Clone, Default)] -#[group(skip)] -pub struct Args { - /// RPC server endpoint - #[arg( - long = "rpc-url", - requires = "network_passphrase", - required_unless_present = "network", - env = "STELLAR_RPC_URL", - help_heading = HEADING_RPC, - )] - pub rpc_url: Option, - /// Network passphrase to sign the transaction sent to the rpc server - #[arg( - long = "network-passphrase", - requires = "rpc_url", - required_unless_present = "network", - env = "STELLAR_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - pub network_passphrase: Option, - /// Name of network to use from config - #[arg( - long, - required_unless_present = "rpc_url", - env = "STELLAR_NETWORK", - help_heading = HEADING_RPC, - )] - pub network: Option, -} - -impl Args { - pub fn get(&self, locator: &locator::Args) -> Result { - if let Some(name) = self.network.as_deref() { - if let Ok(network) = locator.read_network(name) { - return Ok(network); - } - } - if let (Some(rpc_url), Some(network_passphrase)) = - (self.rpc_url.clone(), self.network_passphrase.clone()) - { - Ok(Network { - rpc_url, - network_passphrase, - }) - } else { - Err(Error::Network) - } - } -} - -#[derive(Debug, clap::Args, Serialize, Deserialize, Clone)] -#[group(skip)] -pub struct Network { - /// RPC server endpoint - #[arg( - long = "rpc-url", - env = "STELLAR_RPC_URL", - help_heading = HEADING_RPC, - )] - pub rpc_url: String, - /// Network passphrase to sign the transaction sent to the rpc server - #[arg( - long, - env = "STELLAR_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - pub network_passphrase: String, -} - -impl Network { - pub async fn helper_url(&self, addr: &str) -> Result { - use http::Uri; - tracing::debug!("address {addr:?}"); - let rpc_uri = Uri::from_str(&self.rpc_url) - .map_err(|_| Error::InvalidUrl(self.rpc_url.to_string()))?; - if self.network_passphrase.as_str() == LOCAL_NETWORK_PASSPHRASE { - let auth = rpc_uri.authority().unwrap().clone(); - let scheme = rpc_uri.scheme_str().unwrap(); - Ok(Uri::builder() - .authority(auth) - .scheme(scheme) - .path_and_query(format!("/friendbot?addr={addr}")) - .build()?) - } else { - let client = Client::new(&self.rpc_url)?; - let network = client.get_network().await?; - tracing::debug!("network {network:?}"); - let uri = client.friendbot_url().await?; - tracing::debug!("URI {uri:?}"); - Uri::from_str(&format!("{uri}?addr={addr}")).map_err(|e| { - tracing::error!("{e}"); - Error::InvalidUrl(uri.to_string()) - }) - } - } - - #[allow(clippy::similar_names)] - pub async fn fund_address(&self, addr: &PublicKey) -> Result<(), Error> { - let uri = self.helper_url(&addr.to_string()).await?; - tracing::debug!("URL {uri:?}"); - let response = match uri.scheme_str() { - Some("http") => hyper::Client::new().get(uri.clone()).await?, - Some("https") => { - let https = hyper_tls::HttpsConnector::new(); - hyper::Client::builder() - .build::<_, hyper::Body>(https) - .get(uri.clone()) - .await? - } - _ => { - return Err(Error::InvalidUrl(uri.to_string())); - } - }; - let body = hyper::body::to_bytes(response.into_body()).await?; - let res = serde_json::from_slice::(&body) - .map_err(|e| Error::FailedToParseJSON(uri.to_string(), e))?; - tracing::debug!("{res:#?}"); - if let Some(detail) = res.get("detail").and_then(Value::as_str) { - if detail.contains("createAccountAlreadyExist") { - eprintln!("Account already exists"); - } - } else if res.get("successful").is_none() { - return Err(Error::InproperResponse(res.to_string())); - } - Ok(()) - } - - pub fn rpc_uri(&self) -> Result { - http::Uri::from_str(&self.rpc_url).map_err(|_| Error::InvalidUrl(self.rpc_url.to_string())) - } -} - -pub static DEFAULTS: phf::Map<&'static str, (&'static str, &'static str)> = phf_map! { - "local" => ( - "http://localhost:8000/rpc", - "Standalone Network ; February 2017", - ), - "futurenet" => ( - "https://rpc-futurenet.stellar.org:443", - "Test SDF Future Network ; October 2022", - ), - "testnet" => ( - "https://soroban-testnet.stellar.org", - "Test SDF Network ; September 2015", - ), - "mainnet" => ( - "Bring Your Own: https://developers.stellar.org/docs/data/rpc/rpc-providers", - "Public Global Stellar Network ; September 2015", - ), -}; - -impl From<&(&str, &str)> for Network { - /// Convert the return value of `DEFAULTS.get()` into a Network - fn from(n: &(&str, &str)) -> Self { - Self { - rpc_url: n.0.to_string(), - network_passphrase: n.1.to_string(), - } - } -} diff --git a/cmd/soroban-cli/src/commands/tx/hash.rs b/cmd/soroban-cli/src/commands/tx/hash.rs index d5b2c42e6..8d8ec6d82 100644 --- a/cmd/soroban-cli/src/commands/tx/hash.rs +++ b/cmd/soroban-cli/src/commands/tx/hash.rs @@ -1,6 +1,7 @@ -use crate::{commands::global, utils::transaction_hash}; use hex; +use crate::{commands::global, config::network, utils::transaction_hash}; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -8,7 +9,7 @@ pub enum Error { #[error(transparent)] XdrToBase64(#[from] soroban_env_host::xdr::Error), #[error(transparent)] - Config(#[from] super::super::network::Error), + Config(#[from] network::Error), } // Command to return the transaction hash submitted to a network @@ -17,7 +18,7 @@ pub enum Error { #[group(skip)] pub struct Cmd { #[clap(flatten)] - pub network: super::super::network::Args, + pub network: network::Args, } impl Cmd { diff --git a/cmd/soroban-cli/src/commands/config/alias.rs b/cmd/soroban-cli/src/config/alias.rs similarity index 100% rename from cmd/soroban-cli/src/commands/config/alias.rs rename to cmd/soroban-cli/src/config/alias.rs diff --git a/cmd/soroban-cli/src/commands/config/data.rs b/cmd/soroban-cli/src/config/data.rs similarity index 100% rename from cmd/soroban-cli/src/commands/config/data.rs rename to cmd/soroban-cli/src/config/data.rs diff --git a/cmd/soroban-cli/src/commands/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs similarity index 99% rename from cmd/soroban-cli/src/commands/config/locator.rs rename to cmd/soroban-cli/src/config/locator.rs index cbcb9e8b3..79c72f275 100644 --- a/cmd/soroban-cli/src/commands/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -63,7 +63,7 @@ pub enum Error { #[error(transparent)] String(#[from] std::string::FromUtf8Error), #[error(transparent)] - Secret(#[from] crate::commands::config::secret::Error), + Secret(#[from] crate::config::secret::Error), #[error(transparent)] Json(#[from] serde_json::Error), #[error("cannot access config dir for alias file")] diff --git a/cmd/soroban-cli/src/commands/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs similarity index 97% rename from cmd/soroban-cli/src/commands/config/mod.rs rename to cmd/soroban-cli/src/config/mod.rs index bb55103c1..3632d7e31 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -13,17 +13,14 @@ use crate::{ use self::{network::Network, secret::Secret}; -use super::{keys, network}; - pub mod alias; pub mod data; pub mod locator; +pub mod network; pub mod secret; #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - Identity(#[from] keys::Error), #[error(transparent)] Network(#[from] network::Error), #[error(transparent)] diff --git a/cmd/soroban-cli/src/config/network.rs b/cmd/soroban-cli/src/config/network.rs new file mode 100644 index 000000000..ba63c016f --- /dev/null +++ b/cmd/soroban-cli/src/config/network.rs @@ -0,0 +1,200 @@ +use std::str::FromStr; + +use clap::arg; +use phf::phf_map; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use stellar_strkey::ed25519::PublicKey; + +use crate::{ + commands::HEADING_RPC, + rpc::{self, Client}, +}; + +use super::locator; +pub mod passphrase; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + + #[error("network arg or rpc url and network passphrase are required if using the network")] + Network, + #[error(transparent)] + Http(#[from] http::Error), + #[error(transparent)] + Rpc(#[from] rpc::Error), + #[error(transparent)] + Hyper(#[from] hyper::Error), + #[error("Failed to parse JSON from {0}, {1}")] + FailedToParseJSON(String, serde_json::Error), + #[error("Invalid URL {0}")] + InvalidUrl(String), + #[error("Inproper response {0}")] + InproperResponse(String), + #[error("Currently not supported on windows. Please visit:\n{0}")] + WindowsNotSupported(String), +} + +#[derive(Debug, clap::Args, Clone, Default)] +#[group(skip)] +pub struct Args { + /// RPC server endpoint + #[arg( + long = "rpc-url", + requires = "network_passphrase", + required_unless_present = "network", + env = "STELLAR_RPC_URL", + help_heading = HEADING_RPC, + )] + pub rpc_url: Option, + /// Network passphrase to sign the transaction sent to the rpc server + #[arg( + long = "network-passphrase", + requires = "rpc_url", + required_unless_present = "network", + env = "STELLAR_NETWORK_PASSPHRASE", + help_heading = HEADING_RPC, + )] + pub network_passphrase: Option, + /// Name of network to use from config + #[arg( + long, + required_unless_present = "rpc_url", + env = "STELLAR_NETWORK", + help_heading = HEADING_RPC, + )] + pub network: Option, +} + +impl Args { + pub fn get(&self, locator: &locator::Args) -> Result { + if let Some(name) = self.network.as_deref() { + if let Ok(network) = locator.read_network(name) { + return Ok(network); + } + } + if let (Some(rpc_url), Some(network_passphrase)) = + (self.rpc_url.clone(), self.network_passphrase.clone()) + { + Ok(Network { + rpc_url, + network_passphrase, + }) + } else { + Err(Error::Network) + } + } +} + +#[derive(Debug, clap::Args, Serialize, Deserialize, Clone)] +#[group(skip)] +pub struct Network { + /// RPC server endpoint + #[arg( + long = "rpc-url", + env = "STELLAR_RPC_URL", + help_heading = HEADING_RPC, + )] + pub rpc_url: String, + /// Network passphrase to sign the transaction sent to the rpc server + #[arg( + long, + env = "STELLAR_NETWORK_PASSPHRASE", + help_heading = HEADING_RPC, + )] + pub network_passphrase: String, +} + +impl Network { + pub async fn helper_url(&self, addr: &str) -> Result { + use http::Uri; + tracing::debug!("address {addr:?}"); + let rpc_uri = Uri::from_str(&self.rpc_url) + .map_err(|_| Error::InvalidUrl(self.rpc_url.to_string()))?; + if self.network_passphrase.as_str() == passphrase::LOCAL { + let auth = rpc_uri.authority().unwrap().clone(); + let scheme = rpc_uri.scheme_str().unwrap(); + Ok(Uri::builder() + .authority(auth) + .scheme(scheme) + .path_and_query(format!("/friendbot?addr={addr}")) + .build()?) + } else { + let client = Client::new(&self.rpc_url)?; + let network = client.get_network().await?; + tracing::debug!("network {network:?}"); + let uri = client.friendbot_url().await?; + tracing::debug!("URI {uri:?}"); + Uri::from_str(&format!("{uri}?addr={addr}")).map_err(|e| { + tracing::error!("{e}"); + Error::InvalidUrl(uri.to_string()) + }) + } + } + + #[allow(clippy::similar_names)] + pub async fn fund_address(&self, addr: &PublicKey) -> Result<(), Error> { + let uri = self.helper_url(&addr.to_string()).await?; + tracing::debug!("URL {uri:?}"); + let response = match uri.scheme_str() { + Some("http") => hyper::Client::new().get(uri.clone()).await?, + Some("https") => { + let https = hyper_tls::HttpsConnector::new(); + hyper::Client::builder() + .build::<_, hyper::Body>(https) + .get(uri.clone()) + .await? + } + _ => { + return Err(Error::InvalidUrl(uri.to_string())); + } + }; + let body = hyper::body::to_bytes(response.into_body()).await?; + let res = serde_json::from_slice::(&body) + .map_err(|e| Error::FailedToParseJSON(uri.to_string(), e))?; + tracing::debug!("{res:#?}"); + if let Some(detail) = res.get("detail").and_then(Value::as_str) { + if detail.contains("createAccountAlreadyExist") { + eprintln!("Account already exists"); + } + } else if res.get("successful").is_none() { + return Err(Error::InproperResponse(res.to_string())); + } + Ok(()) + } + + pub fn rpc_uri(&self) -> Result { + http::Uri::from_str(&self.rpc_url).map_err(|_| Error::InvalidUrl(self.rpc_url.to_string())) + } +} + +pub static DEFAULTS: phf::Map<&'static str, (&'static str, &'static str)> = phf_map! { + "local" => ( + "http://localhost:8000/rpc", + passphrase::LOCAL, + ), + "futurenet" => ( + "https://rpc-futurenet.stellar.org:443", + passphrase::FUTURENET, + ), + "testnet" => ( + "https://soroban-testnet.stellar.org", + passphrase::TESTNET, + ), + "mainnet" => ( + "Bring Your Own: https://developers.stellar.org/docs/data/rpc/rpc-providers", + passphrase::MAINNET, + ), +}; + +impl From<&(&str, &str)> for Network { + /// Convert the return value of `DEFAULTS.get()` into a Network + fn from(n: &(&str, &str)) -> Self { + Self { + rpc_url: n.0.to_string(), + network_passphrase: n.1.to_string(), + } + } +} diff --git a/cmd/soroban-cli/src/config/network/passphrase.rs b/cmd/soroban-cli/src/config/network/passphrase.rs new file mode 100644 index 000000000..bdb534cdb --- /dev/null +++ b/cmd/soroban-cli/src/config/network/passphrase.rs @@ -0,0 +1,4 @@ +pub const LOCAL: &str = "Standalone Network ; February 2017"; +pub const TESTNET: &str = "Test SDF Network ; September 2015"; +pub const FUTURENET: &str = "Test SDF Future Network ; October 2022"; +pub const MAINNET: &str = "Public Global Stellar Network ; September 2015"; diff --git a/cmd/soroban-cli/src/commands/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs similarity index 100% rename from cmd/soroban-cli/src/commands/config/secret.rs rename to cmd/soroban-cli/src/config/secret.rs diff --git a/cmd/soroban-cli/src/get_spec.rs b/cmd/soroban-cli/src/get_spec.rs index e21722794..cd9477de2 100644 --- a/cmd/soroban-cli/src/get_spec.rs +++ b/cmd/soroban-cli/src/get_spec.rs @@ -7,9 +7,8 @@ use soroban_env_host::xdr::{ use soroban_spec::read::FromWasmError; pub use soroban_spec_tools::contract as contract_spec; -use crate::commands::config::{self, locator}; -use crate::commands::network; -use crate::commands::{config::data, global}; +use crate::commands::global; +use crate::config::{self, data, locator, network}; use crate::rpc; #[derive(thiserror::Error, Debug)] diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index f71ce55e2..0268a40e0 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -12,6 +12,7 @@ mod cli; pub use cli::main; pub mod commands; +pub mod config; pub mod fee; pub mod get_spec; pub mod key;