From 4b3fac88958594cd159a9e1ab87910e25c43eb91 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Fri, 1 Mar 2024 10:25:32 -0500 Subject: [PATCH] Feat/rpc tests (#1202) * chore: update dependencies and refactor test code The changes in this commit update the dependencies and refactor the test code to improve readability and maintainability. chore: remove unused code and options from test script * fix: docs * fix: consolidate the rpc testing with bindings * fix: set SOROBAN_FEE=10000 * fix: change port to allow for local CI testing and increase fee * fix: port 8000 important * fix: use newer version of RPC * fix: increase fee again * fix: update quickstart to latest latest * fix: deploy contract directly and remove fee args * fix: fmt and ensure stderr for deployoy * fix: don't install contract if it is already installed * fix: try fee arg * fix: fmt * fix: using quickstart:testing * fix: add sleep to dotenv tests * fix: add more sleep to dotenv tests * fix: use instruction and remove sleep * fix: use u32::max * :fix: u32::max/2 * fix: add padding to instruction from sim if one is not provided * fix: make async fn * fix: add back sleep * fix: add wait back in * fix: md-gen * fix: use async_trait --- .github/workflows/rpc-tests.yml | 42 +++ Cargo.lock | 66 ++++- Cargo.toml | 1 + cmd/crates/soroban-test/Cargo.toml | 6 +- cmd/crates/soroban-test/src/lib.rs | 181 +++++++++--- cmd/crates/soroban-test/tests/it/config.rs | 44 +-- .../soroban-test/tests/it/hello_world.rs | 22 -- .../tests/it/integration/custom_types.rs | 10 +- .../tests/it/integration/dotenv.rs | 109 ++++---- .../tests/it/integration/hello_world.rs | 258 ++++++++++-------- .../soroban-test/tests/it/integration/util.rs | 102 ++----- .../soroban-test/tests/it/integration/wrap.rs | 178 ++++++------ cmd/crates/soroban-test/tests/it/main.rs | 2 +- cmd/crates/soroban-test/tests/it/util.rs | 19 +- cmd/soroban-cli/Cargo.toml | 14 +- cmd/soroban-cli/src/bin/main.rs | 1 + .../src/commands/contract/deploy/asset.rs | 27 +- .../src/commands/contract/deploy/wasm.rs | 35 ++- .../src/commands/contract/extend.rs | 23 +- .../src/commands/contract/fetch.rs | 27 +- cmd/soroban-cli/src/commands/contract/init.rs | 2 +- .../src/commands/contract/install.rs | 41 ++- .../src/commands/contract/invoke.rs | 73 +++-- cmd/soroban-cli/src/commands/contract/read.rs | 31 ++- .../src/commands/contract/restore.rs | 21 +- cmd/soroban-cli/src/commands/events.rs | 47 +++- cmd/soroban-cli/src/commands/keys/generate.rs | 10 +- cmd/soroban-cli/src/commands/mod.rs | 13 + cmd/soroban-cli/src/commands/network/mod.rs | 30 +- cmd/soroban-cli/src/fee.rs | 16 +- 30 files changed, 884 insertions(+), 567 deletions(-) create mode 100644 .github/workflows/rpc-tests.yml delete mode 100644 cmd/crates/soroban-test/tests/it/hello_world.rs diff --git a/.github/workflows/rpc-tests.yml b/.github/workflows/rpc-tests.yml new file mode 100644 index 0000000000..a388754e9a --- /dev/null +++ b/.github/workflows/rpc-tests.yml @@ -0,0 +1,42 @@ + +name: RPC Tests +on: + push: + branches: [main, release/**] + pull_request: + +jobs: + test: + name: test RPC + runs-on: ubuntu-22.04 + services: + rpc: + image: stellar/quickstart:testing@sha256:7f074dddaf081b21d273f7346325cc1017c38bbee7b839f8b633b280a663232d + ports: + - 8000:8000 + env: + ENABLE_LOGS: true + NETWORK: local + ENABLE_SOROBAN_RPC: true + options: >- + --health-cmd "curl --no-progress-meter --fail-with-body -X POST \"http://localhost:8000/soroban/rpc\" -H 'Content-Type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":8675309,\"method\":\"getNetwork\"}' && curl --no-progress-meter \"http://localhost:8000/friendbot\" | grep '\"invalid_field\": \"addr\"'" + --health-interval 10s + --health-timeout 5s + --health-retries 50 + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - run: rustup update + - run: cargo build + - run: rustup target add wasm32-unknown-unknown + - run: make build-test-wasms + - run: SOROBAN_PORT=8000 cargo test --features it --package soroban-test --test it -- integration + diff --git a/Cargo.lock b/Cargo.lock index a5b1f458c1..b0c5b4b403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,7 +1184,7 @@ dependencies = [ "gix-date", "itoa", "thiserror", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1266,7 +1266,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1515,7 +1515,7 @@ dependencies = [ "itoa", "smallvec", "thiserror", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1638,7 +1638,7 @@ dependencies = [ "gix-transport", "maybe-async", "thiserror", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -1671,7 +1671,7 @@ dependencies = [ "gix-validate", "memmap2", "thiserror", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -3327,6 +3327,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3509,6 +3518,7 @@ version = "20.3.1" dependencies = [ "assert_cmd", "assert_fs", + "async-trait", "base64 0.21.7", "cargo_metadata", "chrono", @@ -3563,8 +3573,8 @@ dependencies = [ "termcolor_output", "thiserror", "tokio", - "toml", - "toml_edit", + "toml 0.5.11", + "toml_edit 0.21.1", "tracing", "tracing-appender", "tracing-subscriber", @@ -3850,12 +3860,15 @@ dependencies = [ "soroban-cli", "soroban-env-host", "soroban-ledger-snapshot", + "soroban-rpc", "soroban-sdk", "soroban-spec", "soroban-spec-tools 20.3.1", "stellar-strkey 0.0.7", "thiserror", "tokio", + "toml 0.8.10", + "walkdir", "which", ] @@ -4302,11 +4315,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.5", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -4316,7 +4344,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e68c159e8f5ba8a28c4eb7b0c0c190d77bb479047ca713270048145a9ad28a" +dependencies = [ + "indexmap 2.2.3", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.0", ] [[package]] @@ -4935,6 +4976,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1dbce9e90e5404c5a52ed82b1d13fc8cfbdad85033b6f57546ffd1265f8451" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index c2f0f3e6d2..aad28acebe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ sha2 = "0.10.7" ethnum = "1.3.2" hex = "0.4.3" itertools = "0.10.0" +async-trait = "0.1.76" serde-aux = "4.1.2" serde_json = "1.0.82" diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index 81ad8dc040..0886a92749 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -25,6 +25,7 @@ stellar-strkey = { workspace = true } soroban-sdk = { workspace = true } sep5 = { workspace = true } soroban-cli = { workspace = true } +soroban-rpc = { workspace = true } thiserror = "1.0.31" sha2 = "0.10.6" @@ -32,11 +33,14 @@ assert_cmd = "2.0.4" assert_fs = "1.0.7" predicates = "2.1.5" fs_extra = "1.3.0" +toml = "0.8.10" + [dev-dependencies] serde_json = "1.0.93" which = { workspace = true } tokio = "1.28.1" +walkdir = "2.4.0" [features] -integration = [] +it = [] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index bda6ec420f..9ac8841937 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -30,13 +30,17 @@ use assert_fs::{fixture::FixtureError, prelude::PathChild, TempDir}; use fs_extra::dir::CopyOptions; use soroban_cli::{ - commands::{config, contract, contract::invoke, global, keys}, - CommandParser, Pwd, + commands::{config, contract::invoke, global, keys, network, NetworkRunnable}, + CommandParser, }; mod wasm; pub use wasm::Wasm; +pub const TEST_ACCOUNT: &str = "test"; + +pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017"; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] @@ -53,11 +57,16 @@ pub enum Error { /// its own `TempDir` where it will save test-specific configuration. pub struct TestEnv { pub temp_dir: TempDir, + pub rpc_url: String, } impl Default for TestEnv { fn default() -> Self { - Self::new().unwrap() + let temp_dir = TempDir::new().unwrap(); + Self { + temp_dir, + rpc_url: "http://localhost:8889/soroban/rpc".to_string(), + } } } @@ -79,27 +88,67 @@ impl TestEnv { let test_env = TestEnv::default(); f(&test_env); } - pub fn new() -> Result { - let this = TempDir::new().map(|temp_dir| TestEnv { temp_dir })?; - std::env::set_var("XDG_CONFIG_HOME", this.temp_dir.as_os_str()); - this.new_assert_cmd("keys") - .arg("generate") - .arg("test") - .arg("-d") - .arg("--no-fund") - .assert(); - std::env::set_var("SOROBAN_ACCOUNT", "test"); - Ok(this) + + pub fn with_default_network(f: F) { + let test_env = TestEnv::new(); + f(&test_env); + } + + pub fn with_port(host_port: u16) -> TestEnv { + Self::with_rpc_url(&format!("http://localhost:{host_port}/soroban/rpc")) } + pub fn with_rpc_url(rpc_url: &str) -> TestEnv { + let env = TestEnv { + rpc_url: rpc_url.to_string(), + ..Default::default() + }; + env.generate_account("test", None).assert().success(); + env + } + + pub fn new() -> TestEnv { + if let Ok(rpc_url) = std::env::var("SOROBAN_RPC_URL") { + return Self::with_rpc_url(&rpc_url); + } + let host_port = std::env::var("SOROBAN_PORT") + .as_deref() + .ok() + .and_then(|n| n.parse().ok()) + .unwrap_or(8889); + Self::with_port(host_port) + } /// Create a new `assert_cmd::Command` for a given subcommand and set's the current directory /// to be the internal `temp_dir`. pub fn new_assert_cmd(&self, subcommand: &str) -> Command { - let mut this = Command::cargo_bin("soroban").unwrap_or_else(|_| Command::new("soroban")); - this.arg("-q"); - this.arg(subcommand); - this.current_dir(&self.temp_dir); - this + let mut cmd: Command = self.bin(); + cmd.arg(subcommand) + .env("SOROBAN_ACCOUNT", TEST_ACCOUNT) + .env("SOROBAN_RPC_URL", &self.rpc_url) + .env("SOROBAN_NETWORK_PASSPHRASE", LOCAL_NETWORK_PASSPHRASE) + .env("XDG_CONFIG_HOME", self.temp_dir.as_os_str()) + .current_dir(&self.temp_dir); + cmd + } + + pub fn bin(&self) -> Command { + Command::cargo_bin("soroban").unwrap_or_else(|_| Command::new("soroban")) + } + + pub fn generate_account(&self, account: &str, seed: Option) -> Command { + let mut cmd = self.new_assert_cmd("keys"); + cmd.arg("generate").arg(account); + if let Some(seed) = seed { + cmd.arg(format!("--seed={seed}")); + } + cmd + } + + pub fn fund_account(&self, account: &str) -> Assert { + self.new_assert_cmd("keys") + .arg("fund") + .arg(account) + .assert() } /// Parses a `&str` into a command and sets the pwd to be the same as the current `TestEnv`. @@ -129,32 +178,77 @@ impl TestEnv { } /// A convenience method for using the invoke command. - pub async fn invoke>(&self, command_str: &[I]) -> Result { - let cmd = contract::invoke::Cmd::parse_arg_vec( - &command_str - .iter() - .map(AsRef::as_ref) - .filter(|s| !s.is_empty()) - .collect::>(), - ) - .unwrap(); - self.invoke_cmd(cmd).await + pub async fn invoke_with_test>( + &self, + command_str: &[I], + ) -> Result { + self.invoke_with(command_str, "test").await + } + + /// A convenience method for using the invoke command. + pub async fn invoke_with>( + &self, + command_str: &[I], + source: &str, + ) -> Result { + let cmd = self.cmd_with_config::(command_str); + self.run_cmd_with(cmd, source).await + } + + /// A convenience method for using the invoke command. + pub fn cmd_with_config, T: CommandParser + NetworkRunnable>( + &self, + command_str: &[I], + ) -> T { + let mut arg = vec![ + "--network=local", + "--rpc-url=http", + "--network-passphrase=AA", + "--source-account=test", + ]; + let input = command_str + .iter() + .map(AsRef::as_ref) + .filter(|s| !s.is_empty()) + .collect::>(); + arg.extend(input); + T::parse_arg_vec(&arg).unwrap() } /// Invoke an already parsed invoke command - pub async fn invoke_cmd(&self, mut cmd: invoke::Cmd) -> Result { - cmd.set_pwd(self.dir()); - cmd.run_against_rpc_server(&global::Args { + pub async fn run_cmd_with( + &self, + cmd: T, + account: &str, + ) -> Result { + let config_dir = Some(self.dir().to_path_buf()); + let config = config::Args { + network: network::Args { + rpc_url: Some(self.rpc_url.clone()), + network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), + network: None, + }, + source_account: account.to_string(), locator: config::locator::Args { global: false, - config_dir: None, + config_dir: config_dir.clone(), }, - filter_logs: Vec::default(), - quiet: false, - verbose: false, - very_verbose: false, - list: false, - }) + hd_path: None, + }; + cmd.run_against_rpc_server( + Some(&global::Args { + locator: config::locator::Args { + global: false, + config_dir, + }, + filter_logs: Vec::default(), + quiet: false, + verbose: false, + very_verbose: false, + list: false, + }), + Some(&config), + ) .await } @@ -181,7 +275,7 @@ impl TestEnv { /// Copy the contents of the current `TestEnv` to another `TestEnv` pub fn fork(&self) -> Result { - let this = TestEnv::new()?; + let this = TestEnv::new(); self.save(&this.temp_dir)?; Ok(this) } @@ -203,6 +297,7 @@ pub fn temp_ledger_file() -> OsString { pub trait AssertExt { fn stdout_as_str(&self) -> String; + fn stderr_as_str(&self) -> String; } impl AssertExt for Assert { @@ -212,6 +307,12 @@ impl AssertExt for Assert { .trim() .to_owned() } + fn stderr_as_str(&self) -> String { + String::from_utf8(self.get_output().stderr.clone()) + .expect("failed to make str") + .trim() + .to_owned() + } } pub trait CommandExt { fn json_arg(&mut self, j: A) -> &mut Self diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 5912b2cf52..b6f1bff084 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -1,5 +1,5 @@ use assert_fs::TempDir; -use soroban_test::TestEnv; +use soroban_test::{AssertExt, TestEnv}; use std::{fs, path::Path}; use crate::util::{add_key, add_test_id, SecretKind, DEFAULT_SEED_PHRASE}; @@ -7,30 +7,35 @@ use soroban_cli::commands::network; const NETWORK_PASSPHRASE: &str = "Local Sandbox Stellar Network ; September 2022"; +fn ls(sandbox: &TestEnv) -> Vec { + sandbox + .new_assert_cmd("network") + .arg("ls") + .assert() + .stdout_as_str() + .split('\n') + .filter(|s| !s.is_empty()) + .map(ToString::to_string) + .collect::>() +} + #[test] fn set_and_remove_network() { TestEnv::with_default(|sandbox| { add_network(sandbox, "local"); let dir = sandbox.dir().join(".soroban").join("network"); - let read_dir = std::fs::read_dir(dir); - println!("{read_dir:#?}"); - let file = read_dir.unwrap().next().unwrap().unwrap(); + let mut read_dir = std::fs::read_dir(dir).unwrap(); + let file = read_dir.next().unwrap().unwrap(); assert_eq!(file.file_name().to_str().unwrap(), "local.toml"); + let res = ls(sandbox); + assert_eq!(res[0], "local"); + sandbox + .new_assert_cmd("network") + .arg("rm") + .arg("local") + .assert() + .success(); - let res = sandbox.cmd::(""); - let res = res.ls().unwrap(); - assert_eq!(res.len(), 1); - assert_eq!(&res[0], "local"); - - sandbox.cmd::("local").run().unwrap(); - - // sandbox - // .new_assert_cmd("config") - // .arg("network") - // .arg("rm") - // .arg("local") - // .assert() - // .stdout(""); sandbox .new_assert_cmd("network") .arg("ls") @@ -105,7 +110,7 @@ fn set_and_remove_global_network() { #[test] fn multiple_networks() { let sandbox = TestEnv::default(); - let ls = || -> Vec { sandbox.cmd::("").ls().unwrap() }; + let ls = || -> Vec { ls(&sandbox) }; add_network(&sandbox, "local"); println!("{:#?}", ls()); @@ -156,7 +161,6 @@ fn generate_key() { sandbox .new_assert_cmd("keys") .arg("generate") - .arg("--network=futurenet") .arg("--no-fund") .arg("--seed") .arg("0000000000000000") diff --git a/cmd/crates/soroban-test/tests/it/hello_world.rs b/cmd/crates/soroban-test/tests/it/hello_world.rs deleted file mode 100644 index 4c45403a13..0000000000 --- a/cmd/crates/soroban-test/tests/it/hello_world.rs +++ /dev/null @@ -1,22 +0,0 @@ -use soroban_cli::commands::contract::{self, fetch}; -use soroban_test::TestEnv; -use std::path::PathBuf; - -use crate::util::{ - add_test_seed, is_rpc, network_passphrase, network_passphrase_arg, rpc_url, rpc_url_arg, - DEFAULT_PUB_KEY, DEFAULT_PUB_KEY_1, DEFAULT_SECRET_KEY, DEFAULT_SEED_PHRASE, HELLO_WORLD, - TEST_SALT, -}; - -#[tokio::test] -async fn fetch() { - if !is_rpc() { - return; - } - let e = TestEnv::default(); - let f = e.dir().join("contract.wasm"); - let id = deploy_hello(&e); - let cmd = e.cmd_arr::(&["--id", &id, "--out-file", f.to_str().unwrap()]); - cmd.run().await.unwrap(); - assert!(f.exists()); -} diff --git a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs index fda2c1f610..6739ff3da1 100644 --- a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs +++ b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs @@ -3,7 +3,7 @@ use serde_json::json; use soroban_cli::commands; use soroban_test::TestEnv; -use crate::integration::util::{deploy_custom, extend_contract, CUSTOM_TYPES}; +use crate::integration::util::{deploy_custom, extend_contract}; use super::util::invoke_with_roundtrip; @@ -15,9 +15,9 @@ fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command { #[tokio::test] async fn parse() { - let sandbox = &TestEnv::default(); - let id = &deploy_custom(sandbox); - extend_contract(sandbox, id, CUSTOM_TYPES).await; + let sandbox = &TestEnv::new(); + let id = &deploy_custom(sandbox).await; + extend_contract(sandbox, id).await; symbol(sandbox, id); string_with_quotes(sandbox, id).await; symbol_with_quotes(sandbox, id).await; @@ -187,7 +187,7 @@ fn number_arg_return_ok(sandbox: &TestEnv, id: &str) { async fn number_arg_return_err(sandbox: &TestEnv, id: &str) { let res = sandbox - .invoke(&["--id", id, "--", "u32_fail_on_even", "--u32_=2"]) + .invoke_with_test(&["--id", id, "--", "u32_fail_on_even", "--u32_=2"]) .await .unwrap_err(); if let commands::contract::invoke::Error::ContractInvoke(name, doc) = &res { diff --git a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs index 7c0f25b3fc..dff36dfe76 100644 --- a/cmd/crates/soroban-test/tests/it/integration/dotenv.rs +++ b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs @@ -1,67 +1,68 @@ use soroban_test::TestEnv; -use super::util::{deploy_hello, TEST_CONTRACT_ID}; +use super::util::deploy_hello; fn write_env_file(e: &TestEnv, contents: &str) { let env_file = e.dir().join(".env"); - std::fs::write(&env_file, contents).unwrap(); + let contents = format!("SOROBAN_CONTRACT_ID={contents}"); + std::fs::write(&env_file, &contents).unwrap(); assert_eq!(contents, std::fs::read_to_string(env_file).unwrap()); } -fn contract_id() -> String { - format!("SOROBAN_CONTRACT_ID={TEST_CONTRACT_ID}") +#[tokio::test] +async fn can_read_file() { + let e = &TestEnv::new(); + std::thread::sleep(core::time::Duration::from_millis(1000)); + let id = deploy_hello(e).await; + println!("{id}"); + write_env_file(e, &id); + e.new_assert_cmd("contract") + .arg("invoke") + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); } -#[test] -fn can_read_file() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); - e.new_assert_cmd("contract") - .arg("invoke") - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); - }); +#[tokio::test] +async fn current_env_not_overwritten() { + let e = TestEnv::new(); + std::thread::sleep(core::time::Duration::from_millis(3000)); + write_env_file(&e, &deploy_hello(&e).await); + e.new_assert_cmd("contract") + .env( + "SOROBAN_CONTRACT_ID", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + ) + .arg("invoke") + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stderr( + "error: Contract not found: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4\n", + ); } -#[test] -fn current_env_not_overwritten() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); - - e.new_assert_cmd("contract") - .env("SOROBAN_CONTRACT_ID", "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4") - .arg("invoke") - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stderr("error: Contract not found: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4\n"); - }); -} - -#[test] -fn cli_args_have_priority() { - TestEnv::with_default(|e| { - deploy_hello(e); - write_env_file(e, &contract_id()); - e.new_assert_cmd("contract") - .env( - "SOROBAN_CONTRACT_ID", - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - ) - .arg("invoke") - .arg("--id") - .arg(TEST_CONTRACT_ID) - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n"); - }); +#[tokio::test] +async fn cli_args_have_priority() { + let e = &TestEnv::new(); + std::thread::sleep(core::time::Duration::from_millis(2000)); + let id = deploy_hello(e).await; + write_env_file(e, &id); + e.new_assert_cmd("contract") + .env( + "SOROBAN_CONTRACT_ID", + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + ) + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n"); } 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 7bd8d596a7..671d2edffe 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -1,50 +1,115 @@ use predicates::boolean::PredicateBooleanExt; use soroban_cli::commands::{ + config::{locator, secret}, contract::{self, fetch}, - keys, }; -use soroban_test::TestEnv; +use soroban_rpc::GetLatestLedgerResponse; +use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; -use crate::{integration::util::extend_contract, util::DEFAULT_SEED_PHRASE}; +use crate::integration::util::extend_contract; -use super::util::{ - add_test_seed, deploy_hello, extend, network_passphrase, network_passphrase_arg, rpc_url, - rpc_url_arg, DEFAULT_PUB_KEY, DEFAULT_PUB_KEY_1, DEFAULT_SECRET_KEY, HELLO_WORLD, -}; +use super::util::{deploy_hello, extend, HELLO_WORLD}; #[tokio::test] -#[ignore] async fn invoke() { - let sandbox = &TestEnv::default(); - let id = &deploy_hello(sandbox); - extend_contract(sandbox, id, HELLO_WORLD).await; + let sandbox = &TestEnv::new(); + let c = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let GetLatestLedgerResponse { sequence, .. } = c.get_latest_ledger().await.unwrap(); + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .assert() + .stderr(predicates::str::contains("Account already exists")); + sandbox + .new_assert_cmd("keys") + .arg("fund") + .arg("test") + .arg("--hd-path=1") + .assert(); + let addr = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .stdout_as_str(); + let addr_1 = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .arg("--hd-path=1") + .assert() + .stdout_as_str(); + println!("Addrs {addr}, {addr_1}"); + + let secret_key = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("test") + .assert() + .stdout_as_str(); + let secret_key_1 = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("test") + .arg("--hd-path=1") + .assert() + .stdout_as_str(); + let dir = sandbox.dir(); + let seed_phrase = std::fs::read_to_string(dir.join(".soroban/identity/test.toml")).unwrap(); + let s = toml::from_str::(&seed_phrase).unwrap(); + let secret::Secret::SeedPhrase { seed_phrase } = s else { + panic!("Expected seed phrase") + }; + let id = &deploy_hello(sandbox).await; + extend_contract(sandbox, id).await; // Note that all functions tested here have no state invoke_hello_world(sandbox, id); + sandbox .new_assert_cmd("events") - .arg("--start-ledger=20") + .arg("--start-ledger") + .arg(&sequence.to_string()) .arg("--id") .arg(id) .assert() .stdout(predicates::str::contains(id).not()) .success(); invoke_hello_world_with_lib(sandbox, id).await; - sandbox - .new_assert_cmd("events") - .arg("--start-ledger=20") - .arg("--id") - .arg(id) + let config_locator = locator::Args { + global: false, + config_dir: Some(dir.to_path_buf()), + }; + config_locator + .write_identity( + "testone", + &secret::Secret::SecretKey { + secret_key: secret_key_1.clone(), + }, + ) + .unwrap(); + let sk_from_file = std::fs::read_to_string(dir.join(".soroban/identity/testone.toml")).unwrap(); + + assert_eq!(sk_from_file, format!("secret_key = \"{secret_key_1}\"\n")); + let secret_key_1_readin = sandbox + .new_assert_cmd("keys") + .arg("show") + .arg("testone") .assert() - .stdout(predicates::str::contains(id)) - .success(); - invoke_hello_world_with_lib_two(sandbox, id).await; - invoke_auth(sandbox, id); - invoke_auth_with_identity(sandbox, id).await; - invoke_auth_with_different_test_account_fail(sandbox, id).await; + .stdout_as_str(); + assert_eq!(secret_key_1, secret_key_1_readin); + // list all files recursively from dir including in hidden folders + for entry in walkdir::WalkDir::new(dir) { + println!("{}", entry.unwrap().path().display()); + } + invoke_auth(sandbox, id, &addr); + invoke_auth_with_identity(sandbox, id, "test", &addr); + invoke_auth_with_identity(sandbox, id, "testone", &addr_1); + invoke_auth_with_different_test_account_fail(sandbox, id, &addr_1).await; // invoke_auth_with_different_test_account(sandbox, id); contract_data_read_failure(sandbox, id); - invoke_with_seed(sandbox, id).await; - invoke_with_sk(sandbox, id).await; + invoke_with_seed(sandbox, id, &seed_phrase).await; + invoke_with_sk(sandbox, id, &secret_key).await; // This does add an identity to local config invoke_with_id(sandbox, id).await; handles_kebab_case(sandbox, id).await; @@ -68,48 +133,27 @@ fn invoke_hello_world(sandbox: &TestEnv, id: &str) { } async fn invoke_hello_world_with_lib(e: &TestEnv, id: &str) { - let mut cmd = contract::invoke::Cmd { + let cmd = contract::invoke::Cmd { contract_id: id.to_string(), slop: vec!["hello".into(), "--world=world".into()], ..Default::default() }; - - cmd.config.network.rpc_url = rpc_url(); - cmd.config.network.network_passphrase = network_passphrase(); - - let res = e.invoke_cmd(cmd).await.unwrap(); + let res = e.run_cmd_with(cmd, "test").await.unwrap(); assert_eq!(res, r#"["Hello","world"]"#); } -async fn invoke_hello_world_with_lib_two(e: &TestEnv, id: &str) { - let hello_world = HELLO_WORLD.to_string(); - let mut invoke_args = vec!["--id", id, "--wasm", hello_world.as_str()]; - let args = vec!["--", "hello", "--world=world"]; - let res = - if let (Some(rpc), Some(network_passphrase)) = (rpc_url_arg(), network_passphrase_arg()) { - invoke_args.push(&rpc); - invoke_args.push(&network_passphrase); - e.invoke(&[invoke_args, args].concat()).await.unwrap() - } else { - e.invoke(&[invoke_args, args].concat()).await.unwrap() - }; - assert_eq!(res, r#"["Hello","world"]"#); -} - -fn invoke_auth(sandbox: &TestEnv, id: &str) { +fn invoke_auth(sandbox: &TestEnv, id: &str, addr: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") .arg("--id") .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) .arg("--") .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) + .arg("--addr=test") .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); // Invoke it again without providing the contract, to exercise the deployment @@ -120,65 +164,40 @@ fn invoke_auth(sandbox: &TestEnv, id: &str) { .arg(id) .arg("--") .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) + .arg("--addr=test") .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); } -async fn invoke_auth_with_identity(sandbox: &TestEnv, id: &str) { - sandbox - .cmd::("test -d ") - .run() - .await - .unwrap(); +fn invoke_auth_with_identity(sandbox: &TestEnv, id: &str, key: &str, addr: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") + .arg("--source") + .arg(key) .arg("--id") .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) .arg("--") .arg("auth") .arg("--addr") - .arg(DEFAULT_PUB_KEY) + .arg(key) .arg("--world=world") .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .stdout(format!("\"{addr}\"\n")) .success(); } -// fn invoke_auth_with_different_test_account(sandbox: &TestEnv, id: &str) { -// sandbox -// .new_assert_cmd("contract") -// .arg("invoke") -// .arg("--hd-path=1") -// .arg("--id") -// .arg(id) -// .arg("--wasm") -// .arg(HELLO_WORLD.path()) -// .arg("--") -// .arg("auth") -// .arg(&format!("--addr={DEFAULT_PUB_KEY_1}")) -// .arg("--world=world") -// .assert() -// .stdout(format!("\"{DEFAULT_PUB_KEY_1}\"\n")) -// .success(); -// } - -async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &str) { +async fn invoke_auth_with_different_test_account_fail(sandbox: &TestEnv, id: &str, addr: &str) { let res = sandbox - .invoke(&[ + .invoke_with_test(&[ "--hd-path=0", "--id", id, - &rpc_url_arg().unwrap_or_default(), - &network_passphrase_arg().unwrap_or_default(), "--", "auth", - &format!("--addr={DEFAULT_PUB_KEY_1}"), + &format!("--addr={addr}"), "--world=world", ]) .await; @@ -207,9 +226,12 @@ fn contract_data_read_failure(sandbox: &TestEnv, id: &str) { #[tokio::test] async fn contract_data_read() { const KEY: &str = "COUNTER"; - let sandbox = &TestEnv::default(); - let id = &deploy_hello(sandbox); - let res = sandbox.invoke(&["--id", id, "--", "inc"]).await.unwrap(); + let sandbox = &TestEnv::new(); + let id = &deploy_hello(sandbox).await; + let res = sandbox + .invoke_with_test(&["--id", id, "--", "inc"]) + .await + .unwrap(); assert_eq!(res.trim(), "1"); extend(sandbox, id, Some(KEY)).await; @@ -248,30 +270,41 @@ async fn contract_data_read() { .stdout(predicates::str::starts_with("COUNTER,2")); } -async fn invoke_with_seed(sandbox: &TestEnv, id: &str) { - invoke_with_source(sandbox, DEFAULT_SEED_PHRASE, id).await; +#[tokio::test] +#[ignore] +async fn half_max_instructions() { + let sandbox = TestEnv::new(); + let wasm = HELLO_WORLD; + sandbox + .new_assert_cmd("contract") + .arg("deploy") + .arg("--fee") + .arg("1000000") + .arg("--instructions") + .arg(&(u32::MAX / 2).to_string()) + .arg("--wasm") + .arg(wasm.path()) + .arg("--ignore-checks") + .assert() + .stderr("") + .stdout_as_str(); +} + +async fn invoke_with_seed(sandbox: &TestEnv, id: &str, seed_phrase: &str) { + invoke_with_source(sandbox, seed_phrase, id).await; } -async fn invoke_with_sk(sandbox: &TestEnv, id: &str) { - invoke_with_source(sandbox, DEFAULT_SECRET_KEY, id).await; +async fn invoke_with_sk(sandbox: &TestEnv, id: &str, sk: &str) { + invoke_with_source(sandbox, sk, id).await; } async fn invoke_with_id(sandbox: &TestEnv, id: &str) { - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(sandbox, &identity, id).await; + invoke_with_source(sandbox, "test", id).await; } async fn invoke_with_source(sandbox: &TestEnv, source: &str, id: &str) { let cmd = sandbox - .invoke(&[ - "--source-account", - source, - "--id", - id, - "--", - "hello", - "--world=world", - ]) + .invoke_with(&["--id", id, "--", "hello", "--world=world"], source) .await .unwrap(); assert_eq!(cmd, "[\"Hello\",\"world\"]"); @@ -279,25 +312,32 @@ async fn invoke_with_source(sandbox: &TestEnv, source: &str, id: &str) { async fn handles_kebab_case(e: &TestEnv, id: &str) { assert!(e - .invoke(&["--id", id, "--", "multi-word-cmd", "--contract-owner=world",]) + .invoke_with_test(&["--id", id, "--", "multi-word-cmd", "--contract-owner=world",]) .await .is_ok()); } async fn fetch(sandbox: &TestEnv, id: &str) { let f = sandbox.dir().join("contract.wasm"); - let cmd = sandbox.cmd_arr::(&["--id", id, "--out-file", f.to_str().unwrap()]); + let cmd = sandbox.cmd_arr::(&[ + "--rpc-url", + &sandbox.rpc_url, + "--network-passphrase", + LOCAL_NETWORK_PASSPHRASE, + "--id", + id, + "--out-file", + f.to_str().unwrap(), + ]); cmd.run().await.unwrap(); assert!(f.exists()); } async fn invoke_prng_u64_in_range_test(sandbox: &TestEnv, id: &str) { assert!(sandbox - .invoke(&[ + .invoke_with_test(&[ "--id", id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), "--", "prng_u64_in_range", "--low=0", diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index ea27680b76..faa3534e4a 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -1,23 +1,10 @@ -use soroban_cli::commands::contract; +use soroban_cli::commands; use soroban_test::{TestEnv, Wasm}; -use std::{fmt::Display, path::Path}; - -use crate::util::{add_key, SecretKind}; +use std::fmt::Display; pub const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world"); pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types"); -pub fn add_test_seed(dir: &Path) -> String { - let name = "test_seed"; - add_key( - dir, - name, - SecretKind::Seed, - "coral light army gather adapt blossom school alcohol coral light army giggle", - ); - name.to_owned() -} - pub async fn invoke_with_roundtrip(e: &TestEnv, id: &str, func: &str, data: D) where D: Display, @@ -25,80 +12,37 @@ where let data = data.to_string(); println!("{data}"); let res = e - .invoke(&["--id", id, "--", func, &format!("--{func}"), &data]) + .invoke_with_test(&["--id", id, "--", func, &format!("--{func}"), &data]) .await .unwrap(); assert_eq!(res, data); } -pub const DEFAULT_PUB_KEY: &str = "GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4"; -pub const DEFAULT_SECRET_KEY: &str = "SC36BWNUOCZAO7DMEJNNKFV6BOTPJP7IG5PSHLUOLT6DZFRU3D3XGIXW"; - -pub const DEFAULT_PUB_KEY_1: &str = "GCKZUJVUNEFGD4HLFBUNVYM2QY2P5WQQZMGRA3DDL4HYVT5MW5KG3ODV"; pub const TEST_SALT: &str = "f55ff16f66f43360266b95db6f8fec01d76031054306ae4a4b380598f6cfd114"; -pub const TEST_CONTRACT_ID: &str = "CBVTIVBYWAO2HNPNGKDCZW4OZYYESTKNGD7IPRTDGQSFJS4QBDQQJX3T"; - -pub fn rpc_url() -> Option { - std::env::var("SOROBAN_RPC_URL").ok() -} - -pub fn rpc_url_arg() -> Option { - rpc_url().map(|url| format!("--rpc-url={url}")) -} - -pub fn network_passphrase() -> Option { - std::env::var("SOROBAN_NETWORK_PASSPHRASE").ok() -} -pub fn network_passphrase_arg() -> Option { - network_passphrase().map(|p| format!("--network-passphrase={p}")) +pub async fn deploy_hello(sandbox: &TestEnv) -> String { + deploy_contract(sandbox, HELLO_WORLD).await } -pub fn deploy_hello(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, HELLO_WORLD) +pub async fn deploy_custom(sandbox: &TestEnv) -> String { + deploy_contract(sandbox, CUSTOM_TYPES).await } -pub fn deploy_custom(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_TYPES) -} - -pub fn deploy_contract(sandbox: &TestEnv, wasm: &Wasm) -> String { - let hash = wasm.hash().unwrap(); - sandbox - .new_assert_cmd("contract") - .arg("install") - .arg("--wasm") - .arg(wasm.path()) - .arg("--ignore-checks") - .assert() - .success() - .stdout(format!("{hash}\n")); - - sandbox - .new_assert_cmd("contract") - .arg("deploy") - .arg("--wasm-hash") - .arg(&format!("{hash}")) - .arg("--salt") - .arg(TEST_SALT) - .arg("--ignore-checks") - .assert() - .success() - .stdout(format!("{TEST_CONTRACT_ID}\n")); - TEST_CONTRACT_ID.to_string() +pub async fn deploy_contract(sandbox: &TestEnv, wasm: &Wasm<'static>) -> String { + let cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>(&[ + "--fee", + "1000000", + "--wasm", + &wasm.path().to_string_lossy(), + "--salt", + TEST_SALT, + "--ignore-checks", + ]); + sandbox.run_cmd_with(cmd, "test").await.unwrap() } -pub async fn extend_contract(sandbox: &TestEnv, id: &str, wasm: &Wasm<'_>) { +pub async fn extend_contract(sandbox: &TestEnv, id: &str) { extend(sandbox, id, None).await; - let cmd: contract::extend::Cmd = sandbox.cmd_arr(&[ - "--wasm-hash", - wasm.hash().unwrap().to_string().as_str(), - "--durability", - "persistent", - "--ledgers-to-extend", - "100000", - ]); - cmd.run().await.unwrap(); } pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { @@ -114,6 +58,10 @@ pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { args.push("--key"); args.push(value); } - let cmd: contract::extend::Cmd = sandbox.cmd_arr(&args); - cmd.run().await.unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("extend") + .args(args) + .assert() + .success(); } diff --git a/cmd/crates/soroban-test/tests/it/integration/wrap.rs b/cmd/crates/soroban-test/tests/it/integration/wrap.rs index a69e70c7c6..aa356ce99c 100644 --- a/cmd/crates/soroban-test/tests/it/integration/wrap.rs +++ b/cmd/crates/soroban-test/tests/it/integration/wrap.rs @@ -1,97 +1,101 @@ -use soroban_cli::CommandParser; -use soroban_cli::{ - commands::{contract::deploy::asset, keys}, - utils::contract_id_hash_from_asset, -}; -use soroban_test::TestEnv; - -use super::util::network_passphrase; +use soroban_cli::utils::contract_id_hash_from_asset; +use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; #[tokio::test] #[ignore] async fn burn() { - let sandbox = &TestEnv::default(); - let network_passphrase = network_passphrase().unwrap(); - println!("NETWORK_PASSPHRASE: {network_passphrase:?}"); - let address = keys::address::Cmd::parse("test") - .unwrap() - .public_key() - .unwrap(); + let sandbox = &TestEnv::new(); + let network_passphrase = LOCAL_NETWORK_PASSPHRASE.to_string(); + let address = sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .stdout_as_str(); let asset = format!("native:{address}"); - wrap_cmd(&asset).run().await.unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("asset") + .arg("deploy") + .arg("--source=test") + .arg("--asset") + .arg(&asset) + .assert() + .success(); + // wrap_cmd(&asset).run().await.unwrap(); let asset = soroban_cli::utils::parsing::parse_asset(&asset).unwrap(); let hash = contract_id_hash_from_asset(&asset, &network_passphrase).unwrap(); let id = stellar_strkey::Contract(hash.0).to_string(); - assert_eq!( - "CAMTHSPKXZJIRTUXQP5QWJIFH3XIDMKLFAWVQOFOXPTKAW5GKV37ZC4N", - id - ); - assert_eq!( - "true", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "authorized", - "--id", - &address.to_string() - ]) - .await - .unwrap() - ); - assert_eq!( - "\"9223372036854775807\"", - sandbox - .invoke(&[ - "--id", - &id, - "--source", - "test", - "--", - "balance", - "--id", - &address.to_string() - ]) - .await - .unwrap(), - ); - - println!( - "{}", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "burn", - "--id", - &address.to_string(), - "--amount=100" - ]) - .await - .unwrap() - ); - - assert_eq!( - "\"9223372036854775707\"", - sandbox - .invoke(&[ - "--id", - &id, - "--source=test", - "--", - "balance", - "--id", - &address.to_string() - ]) - .await - .unwrap(), - ); -} + println!("{id}, {address}"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775807\"\n"); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .args([ + "--id", + &id, + "--", + "authorized", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("true\n"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775807\"\n"); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .env("RUST_LOGS", "trace") + .args([ + "--source=test", + "--id", + &id, + "--", + "burn", + "--from", + "test", + "--amount=100", + ]) + .assert() + .stdout("") + .stderr(""); -fn wrap_cmd(asset: &str) -> asset::Cmd { - asset::Cmd::parse_arg_vec(&["--source=test", &format!("--asset={asset}")]).unwrap() + println!("hi"); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id, + "--", + "balance", + "--id", + &address.to_string(), + ]) + .assert() + .stdout("\"9223372036854775707\"\n"); } diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index a6b18cb22f..10aea449ca 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -1,7 +1,7 @@ mod arg_parsing; mod config; mod help; -#[cfg(feature = "integration")] +#[cfg(feature = "it")] mod integration; mod plugin; mod util; diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index 112d5f841a..c70091503a 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -4,7 +4,7 @@ use soroban_cli::commands::{ config::{locator::KeyType, secret::Secret}, contract, }; -use soroban_test::{TestEnv, Wasm}; +use soroban_test::{TestEnv, Wasm, TEST_ACCOUNT}; pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types"); @@ -44,7 +44,6 @@ pub fn add_test_id(dir: &Path) -> String { pub const DEFAULT_SEED_PHRASE: &str = "coral light army gather adapt blossom school alcohol coral light army giggle"; -#[allow(dead_code)] pub async fn invoke_custom( sandbox: &TestEnv, id: &str, @@ -52,21 +51,9 @@ pub async fn invoke_custom( arg: &str, wasm: &Path, ) -> Result { - let mut i: contract::invoke::Cmd = sandbox.cmd_arr(&[ - "--id", - id, - "--network", - "futurenet", - "--source", - "default", - "--", - func, - arg, - ]); + let mut i: contract::invoke::Cmd = sandbox.cmd_with_config(&["--id", id, "--", func, arg]); i.wasm = Some(wasm.to_path_buf()); - i.config.network.network = Some("futurenet".to_owned()); - i.invoke(&soroban_cli::commands::global::Args::default()) - .await + sandbox.run_cmd_with(i, TEST_ACCOUNT).await } pub const DEFAULT_CONTRACT_ID: &str = "CDR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OO5Z"; diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index e20a6458f6..846b4a36aa 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -48,13 +48,13 @@ soroban-sdk = { workspace = true } soroban-rpc = { workspace = true } clap = { workspace = true, features = [ - "derive", - "env", - "deprecated", - "string", + "derive", + "env", + "deprecated", + "string", ] } -clap_complete = {workspace = true} - +clap_complete = { workspace = true } +async-trait = { workspace = true } base64 = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -101,7 +101,7 @@ gix = { version = "0.58.0", default-features = false, features = [ "blocking-http-transport-reqwest-rust-tls", "worktree-mutation", ] } -ureq = {version = "2.9.1", features = ["json"]} +ureq = { version = "2.9.1", features = ["json"] } tempfile = "3.8.1" toml_edit = "0.21.0" diff --git a/cmd/soroban-cli/src/bin/main.rs b/cmd/soroban-cli/src/bin/main.rs index 7a87099c07..70eeb61102 100644 --- a/cmd/soroban-cli/src/bin/main.rs +++ b/cmd/soroban-cli/src/bin/main.rs @@ -37,6 +37,7 @@ async fn main() { let builder = fmt::Subscriber::builder() .with_env_filter(e_filter) + .with_ansi(false) .with_writer(std::io::stderr); let subscriber = builder.finish(); diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index 173e9b06f1..dbca1940c7 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -12,7 +12,7 @@ use std::convert::Infallible; use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, rpc::{Client, Error as SorobanRpcError}, utils::{contract_id_hash_from_asset, parsing::parse_asset}, }; @@ -58,21 +58,32 @@ pub struct Cmd { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - // Parse asset - let asset = parse_asset(&self.asset)?; - - let res_str = self.run_against_rpc_server(asset).await?; + let res_str = self.run_against_rpc_server(None, None).await?; println!("{res_str}"); Ok(()) } +} + +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; + + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + // Parse asset + let asset = parse_asset(&self.asset)?; - async fn run_against_rpc_server(&self, asset: Asset) -> Result { - let network = self.config.get_network()?; + let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 0797ac175a..bac7fe2f0e 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -14,7 +14,10 @@ use soroban_env_host::{ HostError, }; -use crate::commands::contract::{self, id::wasm::get_contract_id}; +use crate::commands::{ + contract::{self, id::wasm::get_contract_id}, + global, NetworkRunnable, +}; use crate::{ commands::{config, contract::install, HEADING_RPC}, rpc::{self, Client}, @@ -92,20 +95,31 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = self.run_and_get_contract_id().await?; + let res_str = self.run_against_rpc_server(None, None).await?; println!("{res_str}"); Ok(()) } +} - pub async fn run_and_get_contract_id(&self) -> Result { +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; + + async fn run_against_rpc_server( + &self, + global_args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { let hash = install::Cmd { wasm: wasm::Args { wasm: wasm.clone() }, - config: self.config.clone(), + config: config.clone(), fee: self.fee.clone(), ignore_checks: self.ignore_checks, } - .run_and_get_hash() + .run_against_rpc_server(global_args, Some(config)) .await?; hex::encode(hash) } else { @@ -115,18 +129,13 @@ impl Cmd { .to_string() }; - let hash = Hash(utils::contract_id_from_str(&wasm_hash).map_err(|e| { + let wasm_hash = Hash(utils::contract_id_from_str(&wasm_hash).map_err(|e| { Error::CannotParseWasmHash { wasm_hash: wasm_hash.clone(), error: e, } })?); - - self.run_against_rpc_server(hash).await - } - - async fn run_against_rpc_server(&self, wasm_hash: Hash) -> Result { - let network = self.config.get_network()?; + let network = config.get_network()?; let salt: [u8; 32] = match &self.salt { Some(h) => soroban_spec_tools::utils::padded_hex_from_str(h, 32) .map_err(|_| Error::CannotParseSalt { salt: h.clone() })? @@ -139,7 +148,7 @@ impl Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 965eae4fee..bcf8a90d5b 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -9,7 +9,7 @@ use soroban_env_host::xdr::{ }; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, key, rpc::{self, Client}, wasm, Pwd, @@ -80,7 +80,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let ttl_ledger = self.run_against_rpc_server().await?; + let ttl_ledger = self.run_against_rpc_server(None, None).await?; if self.ttl_ledger_only { println!("{ttl_ledger}"); } else { @@ -99,14 +99,25 @@ impl Cmd { } res } +} - async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = u32; + + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let keys = self.key.parse_keys()?; - let network = &self.config.get_network()?; + let network = &config.get_network()?; let client = Client::new(&network.rpc_url)?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; let extend_to = self.ledgers_to_extend(); // Get the account sequence number diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index 61a82fc473..eefb1b4b89 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -21,6 +21,7 @@ use stellar_strkey::DecodeError; use super::super::config::{self, locator}; use crate::commands::network::{self, Network}; +use crate::commands::{global, NetworkRunnable}; use crate::{ rpc::{self, Client}, utils, Pwd, @@ -115,15 +116,29 @@ impl Cmd { } pub async fn get_bytes(&self) -> Result, Error> { - self.run_against_rpc_server().await + self.run_against_rpc_server(None, None).await } pub fn network(&self) -> Result { Ok(self.network.get(&self.locator)?) } - pub async fn run_against_rpc_server(&self) -> Result, Error> { - let network = self.network()?; + fn contract_id(&self) -> Result<[u8; 32], Error> { + utils::contract_id_from_str(&self.contract_id) + .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) + } +} + +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = Vec; + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result, Error> { + let network = config.map_or_else(|| self.network(), |c| Ok(c.get_network()?))?; tracing::trace!(?network); let contract_id = self.contract_id()?; let client = Client::new(&network.rpc_url)?; @@ -133,13 +148,7 @@ impl Cmd { // async closures are not yet stable Ok(client.get_remote_wasm(&contract_id).await?) } - - fn contract_id(&self) -> Result<[u8; 32], Error> { - utils::contract_id_from_str(&self.contract_id) - .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) - } } - pub fn get_contract_wasm_from_storage( storage: &mut Storage, contract_id: [u8; 32], diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index ef25c08ad2..b5a4ce2c04 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -82,7 +82,7 @@ fn get_valid_examples() -> Result, Error> { let body: ReqBody = get(GITHUB_API_URL) .call() .map_err(|e| { - eprintln!("Error fetching example contracts from soroban-examples repo"); + tracing::warn!("Error fetching example contracts from soroban-examples repo"); Box::new(e) })? .into_json()?; diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 21743a6eee..47a47e1d2f 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -4,12 +4,13 @@ use std::num::ParseIntError; use clap::{command, Parser}; use soroban_env_host::xdr::{ - Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, Memo, MuxedAccount, Operation, - OperationBody, Preconditions, ScMetaEntry, ScMetaV0, SequenceNumber, Transaction, + self, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, Memo, MuxedAccount, + Operation, OperationBody, Preconditions, ScMetaEntry, ScMetaV0, SequenceNumber, Transaction, TransactionExt, TransactionResult, TransactionResultResult, Uint256, VecM, }; use super::restore; +use crate::commands::{global, NetworkRunnable}; use crate::key; use crate::rpc::{self, Client}; use crate::{commands::config, utils, wasm}; @@ -65,17 +66,24 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let res_str = hex::encode(self.run_and_get_hash().await?); + let res_str = hex::encode(self.run_against_rpc_server(None, None).await?); println!("{res_str}"); Ok(()) } +} - pub async fn run_and_get_hash(&self) -> Result { - self.run_against_rpc_server(&self.wasm.read()?).await - } - - async fn run_against_rpc_server(&self, contract: &[u8]) -> Result { - let network = self.config.get_network()?; +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = Hash; + async fn run_against_rpc_server( + &self, + args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let contract = self.wasm.read()?; + let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) @@ -100,7 +108,7 @@ impl Cmd { tracing::warn!("the deployed smart contract {path} was built with Soroban Rust SDK v{rs_sdk_ver}, a release candidate version not intended for use with the Stellar Public Network", path = self.wasm.wasm.display()); } } - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = @@ -109,7 +117,14 @@ impl Cmd { let sequence: i64 = account_details.seq_num.into(); let (tx_without_preflight, hash) = - build_install_contract_code_tx(contract, sequence + 1, self.fee.fee, &key)?; + build_install_contract_code_tx(&contract, sequence + 1, self.fee.fee, &key)?; + + let code_key = + xdr::LedgerKey::ContractCode(xdr::LedgerKeyContractCode { hash: hash.clone() }); + let contract_data = client.get_ledger_entries(&[code_key]).await?; + if !contract_data.entries.unwrap_or_default().is_empty() { + return Ok(hash); + } let txn = client .create_assembled_transaction(&tx_without_preflight) @@ -136,12 +151,12 @@ impl Cmd { wasm_hash: None, durability: super::Durability::Persistent, }, - config: self.config.clone(), + config: config.clone(), fee: self.fee.clone(), ledgers_to_extend: None, ttl_ledger_only: true, } - .run_against_rpc_server() + .run_against_rpc_server(args, None) .await?; } diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index c8692eca02..29637dcbd3 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -28,6 +28,7 @@ use super::super::{ config::{self, locator}, events, }; +use crate::commands::NetworkRunnable; use crate::{commands::global, rpc, Pwd}; use soroban_spec_tools::{contract, Spec}; @@ -255,14 +256,45 @@ impl Cmd { } pub async fn invoke(&self, global_args: &global::Args) -> Result { - self.run_against_rpc_server(global_args).await + self.run_against_rpc_server(Some(global_args), None).await } - pub async fn run_against_rpc_server( + pub fn read_wasm(&self) -> Result>, Error> { + Ok(if let Some(wasm) = self.wasm.as_ref() { + Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?) + } else { + None + }) + } + + pub fn spec_entries(&self) -> Result>, Error> { + self.read_wasm()? + .map(|wasm| { + soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec) + }) + .transpose() + } +} + +impl Cmd { + fn contract_id(&self) -> Result<[u8; 32], Error> { + soroban_spec_tools::utils::contract_id_from_str(&self.contract_id) + .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) + } +} + +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = String; + + async fn run_against_rpc_server( &self, - global_args: &global::Args, + global_args: Option<&global::Args>, + config: Option<&config::Args>, ) -> Result { - let network = self.config.get_network()?; + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let contract_id = self.contract_id()?; let spec_entries = self.spec_entries()?; @@ -274,7 +306,7 @@ impl Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = @@ -302,6 +334,11 @@ impl Cmd { txn.sim_response().events()?, ) } else { + let global::Args { + verbose, + very_verbose, + .. + } = global_args.map(Clone::clone).unwrap_or_default(); let res = client .send_assembled_transaction( txn, @@ -309,8 +346,7 @@ impl Cmd { &signers, &network.network_passphrase, Some(log_events), - (global_args.verbose || global_args.very_verbose || self.fee.cost) - .then_some(log_resources), + (verbose || very_verbose || self.fee.cost).then_some(log_resources), ) .await?; (res.return_value()?, res.contract_events()?) @@ -319,29 +355,6 @@ impl Cmd { crate::log::diagnostic_events(&events, tracing::Level::INFO); output_to_string(&spec, &return_value, &function) } - - pub fn read_wasm(&self) -> Result>, Error> { - Ok(if let Some(wasm) = self.wasm.as_ref() { - Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?) - } else { - None - }) - } - - pub fn spec_entries(&self) -> Result>, Error> { - self.read_wasm()? - .map(|wasm| { - soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec) - }) - .transpose() - } -} - -impl Cmd { - fn contract_id(&self) -> Result<[u8; 32], Error> { - soroban_spec_tools::utils::contract_id_from_str(&self.contract_id) - .map_err(|e| Error::CannotParseContractId(self.contract_id.clone(), e)) - } } fn log_events( diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index f25b6c2c0e..a7b1d07a8b 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -14,7 +14,7 @@ use soroban_env_host::{ use soroban_sdk::xdr::Limits; use crate::{ - commands::config, + commands::{config, global, NetworkRunnable}, key, rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; @@ -91,19 +91,10 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let entries = self.run_against_rpc_server().await?; + let entries = self.run_against_rpc_server(None, None).await?; self.output_entries(&entries) } - async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; - tracing::trace!(?network); - let network = &self.config.get_network()?; - let client = Client::new(&network.rpc_url)?; - let keys = self.key.parse_keys()?; - Ok(client.get_full_ledger_entries(&keys).await?) - } - fn output_entries(&self, entries: &FullLedgerEntries) -> Result<(), Error> { if entries.entries.is_empty() { return Err(Error::NoContractDataEntryFoundForContractID); @@ -178,3 +169,21 @@ impl Cmd { Ok(()) } } + +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = FullLedgerEntries; + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; + tracing::trace!(?network); + let client = Client::new(&network.rpc_url)?; + let keys = self.key.parse_keys()?; + Ok(client.get_full_ledger_entries(&keys).await?) + } +} diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 1385068cc5..869d260154 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -13,6 +13,7 @@ use crate::{ commands::{ config::{self, locator}, contract::extend, + global, NetworkRunnable, }, key, rpc::{self, Client}, @@ -87,7 +88,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = self.run_against_rpc_server().await?; + let expiration_ledger_seq = self.run_against_rpc_server(None, None).await?; if let Some(ledgers_to_extend) = self.ledgers_to_extend { extend::Cmd { @@ -105,14 +106,24 @@ impl Cmd { Ok(()) } +} - pub async fn run_against_rpc_server(&self) -> Result { - let network = self.config.get_network()?; +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = u32; + + async fn run_against_rpc_server( + &self, + _: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { + let config = config.unwrap_or(&self.config); + let network = config.get_network()?; tracing::trace!(?network); let entry_keys = self.key.parse_keys()?; - let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; - let key = self.config.key_pair()?; + let key = config.key_pair()?; // Get the account sequence number let public_strkey = diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index aa46bbe230..42145f5bfd 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -3,7 +3,10 @@ use std::io; use soroban_env_host::xdr::{self, Limits, ReadXdr}; -use super::{config::locator, network}; +use super::{ + config::{self, locator}, + global, network, NetworkRunnable, +}; use crate::{rpc, utils}; #[derive(Parser, Debug, Clone)] @@ -119,6 +122,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] Locator(#[from] locator::Error), + #[error(transparent)] + Config(#[from] config::Error), } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)] @@ -162,7 +167,7 @@ impl Cmd { })?; } - let response = self.run_against_rpc_server().await?; + let response = self.run_against_rpc_server(None, None).await?; for event in &response.events { match self.output { @@ -189,9 +194,33 @@ impl Cmd { Ok(()) } - async fn run_against_rpc_server(&self) -> Result { + fn start(&self) -> Result { + let start = match (self.start_ledger, self.cursor.clone()) { + (Some(start), _) => rpc::EventStart::Ledger(start), + (_, Some(c)) => rpc::EventStart::Cursor(c), + // should never happen because of required_unless_present flags + _ => return Err(Error::MissingStartLedgerAndCursor), + }; + Ok(start) + } +} + +#[async_trait::async_trait] +impl NetworkRunnable for Cmd { + type Error = Error; + type Result = rpc::GetEventsResponse; + + async fn run_against_rpc_server( + &self, + _args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result { let start = self.start()?; - let network = self.network.get(&self.locator)?; + let network = if let Some(config) = config { + Ok(config.get_network()?) + } else { + self.network.get(&self.locator) + }?; let client = rpc::Client::new(&network.rpc_url)?; client @@ -208,14 +237,4 @@ impl Cmd { .await .map_err(Error::Rpc) } - - fn start(&self) -> Result { - let start = match (self.start_ledger, self.cursor.clone()) { - (Some(start), _) => rpc::EventStart::Ledger(start), - (_, Some(c)) => rpc::EventStart::Cursor(c), - // should never happen because of required_unless_present flags - _ => return Err(Error::MissingStartLedgerAndCursor), - }; - Ok(start) - } } diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 07782b2160..159191dc30 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -66,9 +66,13 @@ impl Cmd { if !self.no_fund { let addr = secret.public_key(self.hd_path)?; let network = self.network.get(&self.config_locator)?; - network.fund_address(&addr).await.unwrap_or_else(|_| { - tracing::warn!("Failed to fund address: {addr} on at {}", network.rpc_url); - }); + network + .fund_address(&addr) + .await + .map_err(|e| { + tracing::warn!("fund_address failed: {e}"); + }) + .unwrap_or_default(); } Ok(()) } diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 952869af3b..ae3ac3ecb5 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use async_trait::async_trait; use clap::{command, error::ErrorKind, CommandFactory, FromArgMatches, Parser}; pub mod completion; @@ -158,3 +159,15 @@ pub enum Error { #[error(transparent)] Network(#[from] network::Error), } + +#[async_trait] +pub trait NetworkRunnable { + type Error; + type Result; + + async fn run_against_rpc_server( + &self, + global_args: Option<&global::Args>, + config: Option<&config::Args>, + ) -> Result; +} diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 22cba1904e..a1c0217223 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -12,6 +12,8 @@ use crate::{ use super::config::locator; +pub const LOCAL_NETWORK_PASSPHRASE: &str = "Standalone Network ; February 2017"; + pub mod add; pub mod ls; pub mod rm; @@ -43,6 +45,8 @@ pub enum 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), @@ -139,13 +143,27 @@ pub struct Network { impl Network { pub async fn helper_url(&self, addr: &str) -> Result { + use http::Uri; tracing::debug!("address {addr:?}"); - let client = Client::new(&self.rpc_url)?; - let helper_url_root = client.friendbot_url().await?; - let uri = http::Uri::from_str(&helper_url_root) - .map_err(|_| Error::InvalidUrl(helper_url_root.to_string()))?; - http::Uri::from_str(&format!("{uri:?}?addr={addr}")) - .map_err(|_| Error::InvalidUrl(helper_url_root.to_string())) + 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(); + // format!("{scheme}://{auth}/friendbot"); + Ok(Uri::builder() + .authority(auth) + .scheme(scheme) + .path_and_query(format!("/friendbot?addr={addr}")) + .build()?) + } else { + let client = Client::new(&self.rpc_url)?; + let uri = client.friendbot_url().await?; + Uri::from_str(&format!("{uri:?}?addr={addr}")).map_err(|e| { + tracing::error!("{e}"); + Error::InvalidUrl(uri.to_string()) + }) + } } #[allow(clippy::similar_names)] diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 70f427bb9c..353fe6e5db 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,4 +1,5 @@ use clap::arg; +use soroban_env_host::xdr; use soroban_rpc::Assembled; use crate::commands::HEADING_RPC; @@ -22,11 +23,24 @@ impl Args { if let Some(instructions) = self.instructions { txn.set_max_instructions(instructions) } else { - txn + add_padding_to_instructions(txn) } } } +pub fn add_padding_to_instructions(txn: Assembled) -> Assembled { + let xdr::TransactionExt::V1(xdr::SorobanTransactionData { + resources: xdr::SorobanResources { instructions, .. }, + .. + }) = txn.transaction().ext + else { + return txn; + }; + // Start with 150% + let instructions = (instructions.checked_mul(150 / 100)).unwrap_or(instructions); + txn.set_max_instructions(instructions) +} + impl Default for Args { fn default() -> Self { Self {