From 37379dd2ae014836cd80445c335f11f0f324c088 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 19 Oct 2023 10:17:27 -0400 Subject: [PATCH] Feat!(CLI): remove sandbox (#997) * feat!: remove run_in_sandbox * fix: reorganize tests that rely on network and ignore outside of go test * fix: use feature instead of cfg so that cargo test --list can find tests * fix: move common tests to single functions to skip setup time * feat!: skip wasm in invoke * fix: fully remove all sandbox related types and args --- cmd/crates/soroban-test/Cargo.toml | 3 + cmd/crates/soroban-test/src/lib.rs | 11 +- cmd/crates/soroban-test/tests/it/config.rs | 24 +- .../soroban-test/tests/it/custom_types.rs | 497 ------------------ .../soroban-test/tests/it/hello_world.rs | 461 ---------------- cmd/crates/soroban-test/tests/it/help.rs | 97 ++++ .../soroban-test/tests/it/integration.rs | 4 + .../tests/it/integration/custom_types.rs | 419 +++++++++++++++ .../tests/it/{ => integration}/dotenv.rs | 37 +- .../tests/it/integration/hello_world.rs | 290 ++++++++++ .../soroban-test/tests/it/integration/util.rs | 117 +++++ .../tests/it/integration_and_sandbox.rs | 222 -------- cmd/crates/soroban-test/tests/it/main.rs | 8 +- cmd/crates/soroban-test/tests/it/util.rs | 117 +---- .../src/commands/config/events_file.rs | 236 --------- .../src/commands/config/ledger_file.rs | 55 -- cmd/soroban-cli/src/commands/config/mod.rs | 25 - .../commands/contract/bindings/typescript.rs | 8 +- cmd/soroban-cli/src/commands/contract/bump.rs | 48 +- .../src/commands/contract/deploy.rs | 41 +- .../src/commands/contract/fetch.rs | 26 +- .../src/commands/contract/install.rs | 30 +- .../src/commands/contract/invoke.rs | 205 +------- cmd/soroban-cli/src/commands/contract/mod.rs | 2 +- cmd/soroban-cli/src/commands/contract/read.rs | 31 +- .../src/commands/contract/restore.rs | 12 +- cmd/soroban-cli/src/commands/events.rs | 168 +----- .../src/commands/lab/token/wrap.rs | 39 +- cmd/soroban-cli/src/commands/mod.rs | 7 +- cmd/soroban-cli/src/lib.rs | 1 - cmd/soroban-cli/src/network.rs | 8 - cmd/soroban-cli/src/utils.rs | 266 +--------- cmd/soroban-rpc/internal/test/cli_test.go | 6 +- docs/soroban-cli-full-docs.md | 26 +- 34 files changed, 1036 insertions(+), 2511 deletions(-) delete mode 100644 cmd/crates/soroban-test/tests/it/custom_types.rs create mode 100644 cmd/crates/soroban-test/tests/it/help.rs create mode 100644 cmd/crates/soroban-test/tests/it/integration.rs create mode 100644 cmd/crates/soroban-test/tests/it/integration/custom_types.rs rename cmd/crates/soroban-test/tests/it/{ => integration}/dotenv.rs (63%) create mode 100644 cmd/crates/soroban-test/tests/it/integration/hello_world.rs create mode 100644 cmd/crates/soroban-test/tests/it/integration/util.rs delete mode 100644 cmd/crates/soroban-test/tests/it/integration_and_sandbox.rs delete mode 100644 cmd/soroban-cli/src/commands/config/events_file.rs delete mode 100644 cmd/soroban-cli/src/commands/config/ledger_file.rs delete mode 100644 cmd/soroban-cli/src/network.rs diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index 6d157780..e2a4d4d8 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -37,3 +37,6 @@ fs_extra = "1.3.0" serde_json = "1.0.93" which = { workspace = true } tokio = "1.28.1" + +[features] +integration = [] \ No newline at end of file diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 7cf95c5b..8ec628bf 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -71,7 +71,7 @@ impl TestEnv { /// ```rust,no_run /// use soroban_test::TestEnv; /// TestEnv::with_default(|env| { - /// env.invoke(&["--id", "1", "--", "hello", "--world=world"]).unwrap(); + /// env.new_assert_cmd("contract").args(&["invoke", "--id", "1", "--", "hello", "--world=world"]).assert().success(); /// }); /// ``` /// @@ -122,7 +122,7 @@ impl TestEnv { } /// A convenience method for using the invoke command. - pub fn invoke>(&self, command_str: &[I]) -> Result { + pub async fn invoke>(&self, command_str: &[I]) -> Result { let cmd = contract::invoke::Cmd::parse_arg_vec( &command_str .iter() @@ -131,13 +131,13 @@ impl TestEnv { .collect::>(), ) .unwrap(); - self.invoke_cmd(cmd) + self.invoke_cmd(cmd).await } /// Invoke an already parsed invoke command - pub fn invoke_cmd(&self, mut cmd: invoke::Cmd) -> Result { + pub async fn invoke_cmd(&self, mut cmd: invoke::Cmd) -> Result { cmd.set_pwd(self.dir()); - cmd.run_in_sandbox(&global::Args { + cmd.run_against_rpc_server(&global::Args { locator: config::locator::Args { global: false, config_dir: None, @@ -148,6 +148,7 @@ impl TestEnv { very_verbose: false, list: false, }) + .await } /// Reference to current directory of the `TestEnv`. diff --git a/cmd/crates/soroban-test/tests/it/config.rs b/cmd/crates/soroban-test/tests/it/config.rs index 80bb0a57..a0a240af 100644 --- a/cmd/crates/soroban-test/tests/it/config.rs +++ b/cmd/crates/soroban-test/tests/it/config.rs @@ -1,8 +1,8 @@ use assert_fs::TempDir; -use soroban_test::{temp_ledger_file, TestEnv}; +use soroban_test::TestEnv; use std::{fs, path::Path}; -use crate::util::{add_identity, add_test_id, SecretKind, DEFAULT_SEED_PHRASE, HELLO_WORLD}; +use crate::util::{add_identity, add_test_id, SecretKind, DEFAULT_SEED_PHRASE}; use soroban_cli::commands::config::network; const NETWORK_PASSPHRASE: &str = "Local Sandbox Stellar Network ; September 2022"; @@ -205,26 +205,6 @@ fn seed_phrase() { .stdout("test_seed\n"); } -#[test] -fn use_different_ledger_file() { - let sandbox = TestEnv::default(); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id=1") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--ledger-file") - .arg(temp_ledger_file()) - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); - assert!(fs::read(sandbox.dir().join(".soroban/ledger.json")).is_err()); -} - #[test] fn read_address() { let sandbox = TestEnv::default(); diff --git a/cmd/crates/soroban-test/tests/it/custom_types.rs b/cmd/crates/soroban-test/tests/it/custom_types.rs deleted file mode 100644 index 839b326f..00000000 --- a/cmd/crates/soroban-test/tests/it/custom_types.rs +++ /dev/null @@ -1,497 +0,0 @@ -use serde_json::json; - -use soroban_cli::commands; -use soroban_test::TestEnv; - -use crate::util::{invoke, invoke_with_roundtrip, CUSTOM_TYPES}; - -#[test] -fn symbol() { - invoke(&TestEnv::default(), "hello") - .arg("--hello") - .arg("world") - .assert() - .success() - .stdout( - r#""world" -"#, - ); -} - -#[test] -fn string_with_quotes() { - invoke_with_roundtrip("string", json!("hello world")); -} - -#[test] -fn symbol_with_quotes() { - invoke_with_roundtrip("hello", json!("world")); -} - -#[test] -fn generate_help() { - invoke(&TestEnv::default(), "strukt_hel") - .arg("--help") - .assert() - .success() - .stdout(predicates::str::contains( - "Example contract method which takes a struct", - )); -} - -#[test] -fn multi_arg_failure() { - invoke(&TestEnv::default(), "multi_args") - .arg("--b") - .assert() - .failure() - .stderr("error: Missing argument a\n"); -} - -#[test] -fn multi_arg_success() { - invoke(&TestEnv::default(), "multi_args") - .arg("--a") - .arg("42") - .arg("--b") - .assert() - .success() - .stdout("42\n"); -} - -#[test] -fn bytes_as_file() { - let env = &TestEnv::default(); - let path = env.temp_dir.join("bytes.txt"); - std::fs::write(&path, 0x0073_7465_6c6c_6172u128.to_be_bytes()).unwrap(); - invoke(env, "bytes") - .arg("--bytes-file-path") - .arg(path) - .assert() - .success() - .stdout("\"0000000000000000007374656c6c6172\"\n"); -} - -#[test] -fn map() { - invoke_with_roundtrip("map", json!({"0": true, "1": false})); -} - -#[test] -fn map_help() { - invoke(&TestEnv::default(), "map") - .arg("--help") - .assert() - .success() - .stdout(predicates::str::contains("Map")); -} - -#[test] -fn vec_() { - invoke_with_roundtrip("vec", json!([0, 1])); -} - -#[test] -fn vec_help() { - invoke(&TestEnv::default(), "vec") - .arg("--help") - .assert() - .success() - .stdout(predicates::str::contains("Array")); -} - -#[test] -fn tuple() { - invoke_with_roundtrip("tuple", json!(["hello", 0])); -} - -#[test] -fn tuple_help() { - invoke(&TestEnv::default(), "tuple") - .arg("--help") - .assert() - .success() - .stdout(predicates::str::contains("Tuple")); -} - -#[test] -fn strukt() { - invoke_with_roundtrip("strukt", json!({"a": 42, "b": true, "c": "world"})); -} - -#[test] -fn tuple_strukt() { - invoke_with_roundtrip( - "tuple_strukt", - json!([{"a": 42, "b": true, "c": "world"}, "First"]), - ); -} - -#[test] -fn strukt_help() { - invoke(&TestEnv::default(), "strukt") - .arg("--help") - .assert() - .stdout(predicates::str::contains( - "--strukt '{ \"a\": 1, \"b\": true, \"c\": \"hello\" }'", - )) - .stdout(predicates::str::contains( - "This is from the rust doc above the struct Test", - )); -} - -#[test] -fn complex_enum_help() { - invoke(&TestEnv::default(), "complex") - .arg("--help") - .assert() - .stdout(predicates::str::contains( - r#"--complex '{"Struct":{ "a": 1, "b": true, "c": "hello" }}"#, - )) - .stdout(predicates::str::contains( - r#"{"Tuple":[{ "a": 1, "b": true, "c": "hello" }"#, - )) - .stdout(predicates::str::contains( - r#"{"Enum":"First"|"Second"|"Third"}"#, - )) - .stdout(predicates::str::contains( - r#"{"Asset":["GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4", "-100"]}"#, - )) - .stdout(predicates::str::contains(r#""Void"'"#)); -} - -#[test] -fn enum_2_str() { - invoke_with_roundtrip("simple", json!("First")); -} - -#[test] -fn e_2_s_enum() { - invoke_with_roundtrip("complex", json!({"Enum": "First"})); -} - -#[test] -fn asset() { - invoke_with_roundtrip( - "complex", - json!({"Asset": ["CB64D3G7SM2RTH6JSGG34DDTFTQ5CFDKVDZJZSODMCX4NJ2HV2KN7OHT", "100" ]}), - ); -} - -fn complex_tuple() -> serde_json::Value { - json!({"Tuple": [{"a": 42, "b": true, "c": "world"}, "First"]}) -} - -#[test] -fn e_2_s_tuple() { - invoke_with_roundtrip("complex", complex_tuple()); -} - -#[test] -fn e_2_s_strukt() { - invoke_with_roundtrip( - "complex", - json!({"Struct": {"a": 42, "b": true, "c": "world"}}), - ); -} - -#[test] -fn number_arg() { - invoke_with_roundtrip("u32_", 42); -} - -#[test] -fn number_arg_return_ok() { - invoke(&TestEnv::default(), "u32_fail_on_even") - .arg("--u32_") - .arg("1") - .assert() - .success() - .stdout("1\n"); -} - -#[test] -fn number_arg_return_err() { - TestEnv::with_default(|sandbox| { - // matches!(res, commands::invoke::Error) - let p = CUSTOM_TYPES.path(); - let wasm = p.to_str().unwrap(); - let res = sandbox - .invoke(&[ - "--id=1", - "--wasm", - wasm, - "--", - "u32_fail_on_even", - "--u32_=2", - ]) - .unwrap_err(); - if let commands::contract::invoke::Error::ContractInvoke(name, doc) = &res { - assert_eq!(name, "NumberMustBeOdd"); - assert_eq!(doc, "Please provide an odd number"); - }; - println!("{res:#?}"); - }); -} - -#[test] -fn void() { - invoke(&TestEnv::default(), "woid") - .assert() - .success() - .stdout("\n") - .stderr(""); -} - -#[test] -fn val() { - invoke(&TestEnv::default(), "val") - .assert() - .success() - .stdout("null\n") - .stderr(""); -} - -#[test] -fn i32() { - invoke_with_roundtrip("i32_", 42); -} - -#[test] -fn handle_arg_larger_than_i32_failure() { - invoke(&TestEnv::default(), "i32_") - .arg("--i32_") - .arg(u32::MAX.to_string()) - .assert() - .failure() - .stderr(predicates::str::contains("value is not parseable")); -} - -#[test] -fn handle_arg_larger_than_i64_failure() { - invoke(&TestEnv::default(), "i64_") - .arg("--i64_") - .arg(u64::MAX.to_string()) - .assert() - .failure() - .stderr(predicates::str::contains("value is not parseable")); -} - -#[test] -fn i64() { - invoke_with_roundtrip("i64_", i64::MAX); -} - -#[test] -fn negative_i32() { - invoke_with_roundtrip("i32_", -42); -} - -#[test] -fn negative_i64() { - invoke_with_roundtrip("i64_", i64::MIN); -} - -#[test] -fn account_address() { - invoke_with_roundtrip( - "addresse", - json!("GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS"), - ); -} - -#[test] -fn contract_address() { - invoke_with_roundtrip( - "addresse", - json!("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"), - ); -} - -#[test] -fn bytes() { - invoke_with_roundtrip("bytes", json!("7374656c6c6172")); -} - -#[test] -fn const_enum() { - invoke_with_roundtrip("card", "11"); -} - -#[test] -fn parse_u128() { - let num = "340000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "u128") - .arg("--u128") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn parse_i128() { - let num = "170000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "i128") - .arg("--i128") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn parse_negative_i128() { - let num = "-170000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "i128") - .arg("--i128") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn parse_u256() { - let num = "340000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "u256") - .arg("--u256") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn parse_i256() { - let num = "170000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "i256") - .arg("--i256") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn parse_negative_i256() { - let num = "-170000000000000000000000000000000000000"; - invoke(&TestEnv::default(), "i256") - .arg("--i256") - .arg(num) - .assert() - .success() - .stdout(format!( - r#""{num}" -"#, - )); -} - -#[test] -fn boolean() { - invoke(&TestEnv::default(), "boolean") - .arg("--boolean") - .assert() - .success() - .stdout( - r#"true -"#, - ); -} -#[test] -fn boolean_two() { - invoke(&TestEnv::default(), "boolean") - .arg("--boolean") - .arg("true") - .assert() - .success() - .stdout( - r#"true -"#, - ); -} - -#[test] -fn boolean_no_flag() { - invoke(&TestEnv::default(), "boolean") - .assert() - .success() - .stdout( - r#"false -"#, - ); -} - -#[test] -fn boolean_false() { - invoke(&TestEnv::default(), "boolean") - .arg("--boolean") - .arg("false") - .assert() - .success() - .stdout( - r#"false -"#, - ); -} - -#[test] -fn boolean_not() { - invoke(&TestEnv::default(), "not") - .arg("--boolean") - .assert() - .success() - .stdout( - r#"false -"#, - ); -} - -#[test] -fn boolean_not_no_flag() { - invoke(&TestEnv::default(), "not") - .assert() - .success() - .stdout( - r#"true -"#, - ); -} - -#[test] -fn option_none() { - invoke(&TestEnv::default(), "option") - .assert() - .success() - .stdout( - r#"null -"#, - ); -} - -#[test] -fn option_some() { - invoke(&TestEnv::default(), "option") - .arg("--option=1") - .assert() - .success() - .stdout( - r#"1 -"#, - ); -} diff --git a/cmd/crates/soroban-test/tests/it/hello_world.rs b/cmd/crates/soroban-test/tests/it/hello_world.rs index a87aff89..38b78ee3 100644 --- a/cmd/crates/soroban-test/tests/it/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/hello_world.rs @@ -11,427 +11,12 @@ use crate::util::{ TEST_SALT, }; -#[test] -fn install_wasm_then_deploy_contract() { - let sandbox = TestEnv::default(); - assert_eq!(deploy_hello(&sandbox), TEST_CONTRACT_ID); -} - -const TEST_CONTRACT_ID: &str = "CBVTIVBYWAO2HNPNGKDCZW4OZYYESTKNGD7IPRTDGQSFJS4QBDQQJX3T"; - -fn deploy_hello(sandbox: &TestEnv) -> String { - let hash = HELLO_WORLD.hash().unwrap(); - sandbox - .new_assert_cmd("contract") - .arg("install") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .assert() - .success() - .stdout(format!("{hash}\n")); - - let mut cmd: &mut assert_cmd::Command = &mut sandbox.new_assert_cmd("contract"); - - cmd = cmd.arg("deploy").arg("--wasm-hash").arg(&format!("{hash}")); - if is_rpc() { - cmd = cmd.arg("--salt").arg(TEST_SALT); - } else { - cmd = cmd.arg("--id").arg(TEST_CONTRACT_ID); - } - cmd.assert() - .success() - .stdout(format!("{TEST_CONTRACT_ID}\n")); - TEST_CONTRACT_ID.to_string() -} - -#[test] -fn deploy_contract_with_wasm_file() { - if is_rpc() { - return; - } - TestEnv::default() - .new_assert_cmd("contract") - .arg("deploy") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--id=1") - .assert() - .success() - .stdout("CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM\n"); -} - -#[test] -fn invoke_hello_world_with_deploy_first() { - let sandbox = TestEnv::default(); - let id = deploy_hello(&sandbox); - println!("{id}"); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); -} - -#[test] -fn invoke_hello_world() { - let sandbox = TestEnv::default(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--") - .arg("hello") - .arg("--world=world") - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); -} - -#[test] -fn invoke_hello_world_from_file() { - let sandbox = TestEnv::default(); - let tmp_file = sandbox.temp_dir.join("world.txt"); - std::fs::write(&tmp_file, "world").unwrap(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--") - .arg("hello") - .arg("--world-file-path") - .arg(&tmp_file) - .assert() - .stdout("[\"Hello\",\"world\"]\n") - .success(); -} - -#[test] -fn invoke_hello_world_from_file_fail() { - let sandbox = TestEnv::default(); - let tmp_file = sandbox.temp_dir.join("world.txt"); - std::fs::write(&tmp_file, "world").unwrap(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--") - .arg("hello") - .arg("--world-file-path") - .arg(&tmp_file) - .arg("--world=hello") - .assert() - .stderr(predicates::str::contains("error: the argument '--world-file-path ' cannot be used with '--world '")) - .failure(); -} - -#[test] -fn invoke_hello_world_with_lib() { - TestEnv::with_default(|e| { - let id = deploy_hello(e); - let mut cmd = contract::invoke::Cmd { - contract_id: id, - 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).unwrap(); - assert_eq!(res, r#"["Hello","world"]"#); - }); -} - -#[test] -fn invoke_hello_world_with_lib_two() { - TestEnv::with_default(|e| { - let id = deploy_hello(e); - 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()).unwrap() - } else { - e.invoke(&[invoke_args, args].concat()).unwrap() - }; - assert_eq!(res, r#"["Hello","world"]"#); - }); -} -// #[test] -// fn invoke_hello_world_with_lib_three() { -// let sandbox = TestEnv::default(); -// let builder = invoke::CmdBuilder::new().contract_id("1").wasm(HELLO_WORLD.path()).function("hello").slop(["--hello=world"]).build(); -// std::env::set_current_dir(sandbox.dir()).unwrap(); -// assert_eq!(res.run_in_sandbox().unwrap(), r#"["Hello","world"]"#); -// } - -#[test] -fn invoke_auth() { - let sandbox = TestEnv::default(); - let id = &deploy_hello(&sandbox); - 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("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); - - // Invoke it again without providing the contract, to exercise the deployment - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) - .arg("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); -} - -#[tokio::test] -async fn invoke_auth_with_identity() { - let sandbox = TestEnv::default(); - sandbox - .cmd::("test -d ") - .run() - .await - .unwrap(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--") - .arg("auth") - .arg("--addr=test") - .arg("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); -} - -#[test] -fn invoke_auth_with_different_test_account() { - let sandbox = TestEnv::default(); - let id = deploy_hello(&sandbox); - 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(); -} -#[test] -fn contract_data_read_failure() { - let sandbox = TestEnv::default(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .failure() - .stderr( - "error: no matching contract data entries were found for the specified contract id\n", - ); -} - -#[test] -fn contract_data_read() { - let sandbox = TestEnv::default(); - let id = &deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("inc") - .assert() - .success(); - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .success() - .stdout("COUNTER,1,4,4096\n"); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("inc") - .assert() - .success(); - - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .success() - .stdout("COUNTER,2,4,4096\n"); -} -#[test] -fn invoke_auth_with_different_test_account_fail() { - let sandbox = TestEnv::default(); - let id = &deploy_hello(&sandbox); - let res = sandbox.invoke(&[ - "--hd-path=1", - "--id", - id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), - &rpc_url_arg().unwrap_or_default(), - &network_passphrase_arg().unwrap_or_default(), - "--", - "auth", - &format!("--addr={DEFAULT_PUB_KEY}"), - "--world=world", - ]); - assert!(res.is_err()); - if let Err(e) = res { - assert!( - matches!(e, contract::invoke::Error::Host(_)), - "Expected host error got {e:?}" - ); - }; -} - -#[test] -fn invoke_hello_world_with_seed() { - let sandbox = TestEnv::default(); - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(&sandbox, &identity); -} - -#[test] -fn invoke_with_seed() { - let sandbox = TestEnv::default(); - invoke_with_source(&sandbox, DEFAULT_SEED_PHRASE); -} - -#[test] -fn invoke_with_id() { - let sandbox = TestEnv::default(); - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(&sandbox, &identity); -} - -#[test] -fn invoke_with_sk() { - let sandbox = TestEnv::default(); - invoke_with_source(&sandbox, DEFAULT_SECRET_KEY); -} - -fn invoke_with_source(sandbox: &TestEnv, source: &str) { - let id = &deploy_hello(sandbox); - let cmd = sandbox.invoke(&[ - "--source-account", - source, - "--id", - id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), - &rpc_url_arg().unwrap_or_default(), - &network_passphrase_arg().unwrap_or_default(), - "--", - "hello", - "--world=world", - ]); - assert_eq!(cmd.unwrap(), "[\"Hello\",\"world\"]"); - - // Invoke it again without providing the contract, to exercise the deployment - let cmd = sandbox.invoke(&[ - "--source-account", - source, - "--id", - id, - "--", - "hello", - "--world=world", - ]); - assert_eq!(cmd.unwrap(), "[\"Hello\",\"world\"]"); -} - -#[test] -fn handles_kebab_case() { - let e = TestEnv::default(); - let id = deploy_hello(&e); - assert!(e - .invoke(&[ - "--id", - &id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), - "--", - "multi-word-cmd", - "--contract-owner=world", - ]) - .is_ok()); -} #[tokio::test] async fn fetch() { @@ -445,49 +30,3 @@ async fn fetch() { cmd.run().await.unwrap(); assert!(f.exists()); } - -#[test] -fn build() { - let sandbox = TestEnv::default(); - - let cargo_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let hello_world_contract_path = - cargo_dir.join("tests/fixtures/test-wasms/hello_world/Cargo.toml"); - sandbox - .new_assert_cmd("contract") - .arg("build") - .arg("--manifest-path") - .arg(hello_world_contract_path) - .arg("--profile") - .arg("test-wasms") - .arg("--package") - .arg("test_hello_world") - .assert() - .success(); -} - -#[test] -fn invoke_prng_u64_in_range_test() { - let sandbox = TestEnv::default(); - let res = sandbox - .new_assert_cmd("contract") - .arg("deploy") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .assert() - .success(); - let stdout = String::from_utf8(res.get_output().stdout.clone()).unwrap(); - let id = stdout.trim_end(); - println!("{id}"); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("prng_u64_in_range") - .arg("--low=0") - .arg("--high=100") - .assert() - .success(); -} diff --git a/cmd/crates/soroban-test/tests/it/help.rs b/cmd/crates/soroban-test/tests/it/help.rs new file mode 100644 index 00000000..6d4680e7 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/help.rs @@ -0,0 +1,97 @@ +use soroban_cli::commands::contract; +use soroban_test::TestEnv; + +use crate::util::{invoke_custom as invoke, CUSTOM_TYPES}; + +async fn invoke_custom(func: &str, args: &str) -> Result { + let e = &TestEnv::default(); + invoke(e, "1", func, args, &CUSTOM_TYPES.path()).await +} + +#[tokio::test] +async fn generate_help() { + assert!(invoke_custom("strukt_hel", "--help") + .await + .unwrap() + .contains("Example contract method which takes a struct")); +} + +#[tokio::test] +async fn vec_help() { + assert!(invoke_custom("vec", "--help") + .await + .unwrap() + .contains("Array")); +} + +#[tokio::test] +async fn tuple_help() { + assert!(invoke_custom("tuple", "--help") + .await + .unwrap() + .contains("Tuple")); +} + +#[tokio::test] +async fn strukt_help() { + let output = invoke_custom("strukt", "--help").await.unwrap(); + assert!(output.contains("--strukt '{ \"a\": 1, \"b\": true, \"c\": \"hello\" }'",)); + assert!(output.contains("This is from the rust doc above the struct Test",)); +} + +#[tokio::test] +async fn complex_enum_help() { + let output = invoke_custom("complex", "--help").await.unwrap(); + assert!(output.contains(r#"--complex '{"Struct":{ "a": 1, "b": true, "c": "hello" }}"#,)); + assert!(output.contains(r#"{"Tuple":[{ "a": 1, "b": true, "c": "hello" }"#,)); + assert!(output.contains(r#"{"Enum":"First"|"Second"|"Third"}"#,)); + assert!(output.contains( + r#"{"Asset":["GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4", "-100"]}"#, + )); + assert!(output.contains(r#""Void"'"#)); +} + +#[tokio::test] +async fn multi_arg_failure() { + assert!(matches!( + invoke_custom("multi_args", "--b").await.unwrap_err(), + contract::invoke::Error::MissingArgument(_) + )); +} + +#[tokio::test] +async fn handle_arg_larger_than_i32_failure() { + let res = invoke_custom("i32_", &format!("--i32_={}", u32::MAX)).await; + assert!(matches!( + res, + Err(contract::invoke::Error::CannotParseArg { .. }) + )); +} + +#[tokio::test] +async fn handle_arg_larger_than_i64_failure() { + let res = invoke_custom("i64_", &format!("--i64_={}", u64::MAX)).await; + assert!(matches!( + res, + Err(contract::invoke::Error::CannotParseArg { .. }) + )); +} + +#[test] +fn build() { + let sandbox = TestEnv::default(); + let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let hello_world_contract_path = + cargo_dir.join("tests/fixtures/test-wasms/hello_world/Cargo.toml"); + sandbox + .new_assert_cmd("contract") + .arg("build") + .arg("--manifest-path") + .arg(hello_world_contract_path) + .arg("--profile") + .arg("test-wasms") + .arg("--package") + .arg("test_hello_world") + .assert() + .success(); +} diff --git a/cmd/crates/soroban-test/tests/it/integration.rs b/cmd/crates/soroban-test/tests/it/integration.rs new file mode 100644 index 00000000..d196ce07 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration.rs @@ -0,0 +1,4 @@ +mod custom_types; +mod dotenv; +mod hello_world; +mod util; diff --git a/cmd/crates/soroban-test/tests/it/integration/custom_types.rs b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs new file mode 100644 index 00000000..fe4ef978 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/custom_types.rs @@ -0,0 +1,419 @@ +use serde_json::json; + +use soroban_cli::commands; +use soroban_test::TestEnv; + +use crate::integration::util::{bump_contract, deploy_custom, CUSTOM_TYPES}; + +use super::util::invoke_with_roundtrip; + +fn invoke_custom(e: &TestEnv, id: &str, func: &str) -> assert_cmd::Command { + let mut s = e.new_assert_cmd("contract"); + s.arg("invoke").arg("--id").arg(id).arg("--").arg(func); + s +} + +#[tokio::test] +async fn parse() { + let sandbox = &TestEnv::default(); + let id = &deploy_custom(sandbox); + bump_contract(sandbox, id, CUSTOM_TYPES).await; + symbol(sandbox, id); + string_with_quotes(sandbox, id).await; + symbol_with_quotes(sandbox, id).await; + multi_arg_success(sandbox, id); + bytes_as_file(sandbox, id); + map(sandbox, id).await; + vec_(sandbox, id).await; + tuple(sandbox, id).await; + strukt(sandbox, id).await; + tuple_strukt(sandbox, id).await; + enum_2_str(sandbox, id).await; + e_2_s_enum(sandbox, id).await; + asset(sandbox, id).await; + e_2_s_tuple(sandbox, id).await; + e_2_s_strukt(sandbox, id).await; + number_arg(sandbox, id).await; + number_arg_return_err(sandbox, id).await; + i32(sandbox, id).await; + i64(sandbox, id).await; + negative_i32(sandbox, id).await; + negative_i64(sandbox, id).await; + account_address(sandbox, id).await; + contract_address(sandbox, id).await; + bytes(sandbox, id).await; + const_enum(sandbox, id).await; + number_arg_return_ok(sandbox, id); + void(sandbox, id); + val(sandbox, id); + parse_u128(sandbox, id); + parse_i128(sandbox, id); + parse_negative_i128(sandbox, id); + parse_u256(sandbox, id); + parse_i256(sandbox, id); + parse_negative_i256(sandbox, id); + boolean(sandbox, id); + boolean_two(sandbox, id); + boolean_no_flag(sandbox, id); + boolean_false(sandbox, id); + boolean_not(sandbox, id); + boolean_not_no_flag(sandbox, id); + option_none(sandbox, id); + option_some(sandbox, id); +} + +fn symbol(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "hello") + .arg("--hello") + .arg("world") + .assert() + .success() + .stdout( + r#""world" +"#, + ); +} + +async fn string_with_quotes(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "string", json!("hello world")).await; +} + +async fn symbol_with_quotes(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "hello", json!("world")).await; +} + +fn multi_arg_success(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "multi_args") + .arg("--a") + .arg("42") + .arg("--b") + .assert() + .success() + .stdout("42\n"); +} + +fn bytes_as_file(sandbox: &TestEnv, id: &str) { + let env = &TestEnv::default(); + let path = env.temp_dir.join("bytes.txt"); + std::fs::write(&path, 0x0073_7465_6c6c_6172u128.to_be_bytes()).unwrap(); + invoke_custom(sandbox, id, "bytes") + .arg("--bytes-file-path") + .arg(path) + .assert() + .success() + .stdout("\"0000000000000000007374656c6c6172\"\n"); +} + +async fn map(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "map", json!({"0": true, "1": false})).await; +} + +async fn vec_(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "vec", json!([0, 1])).await; +} + +async fn tuple(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "tuple", json!(["hello", 0])).await; +} + +async fn strukt(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "strukt", + json!({"a": 42, "b": true, "c": "world"}), + ) + .await; +} + +async fn tuple_strukt(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "tuple_strukt", + json!([{"a": 42, "b": true, "c": "world"}, "First"]), + ) + .await; +} + +async fn enum_2_str(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "simple", json!("First")).await; +} + +async fn e_2_s_enum(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "complex", json!({"Enum": "First"})).await; +} + +async fn asset(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "complex", + json!({"Asset": ["CB64D3G7SM2RTH6JSGG34DDTFTQ5CFDKVDZJZSODMCX4NJ2HV2KN7OHT", "100" ]}), + ) + .await; +} + +fn complex_tuple() -> serde_json::Value { + json!({"Tuple": [{"a": 42, "b": true, "c": "world"}, "First"]}) +} + +async fn e_2_s_tuple(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "complex", complex_tuple()).await; +} + +async fn e_2_s_strukt(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "complex", + json!({"Struct": {"a": 42, "b": true, "c": "world"}}), + ) + .await; +} + +async fn number_arg(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "u32_", 42).await; +} + +fn number_arg_return_ok(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "u32_fail_on_even") + .arg("--u32_") + .arg("1") + .assert() + .success() + .stdout("1\n"); +} + +async fn number_arg_return_err(sandbox: &TestEnv, id: &str) { + let res = sandbox + .invoke(&["--id", id, "--", "u32_fail_on_even", "--u32_=2"]) + .await + .unwrap_err(); + if let commands::contract::invoke::Error::ContractInvoke(name, doc) = &res { + assert_eq!(name, "NumberMustBeOdd"); + assert_eq!(doc, "Please provide an odd number"); + }; + println!("{res:#?}"); +} + +fn void(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "woid") + .assert() + .success() + .stdout("\n") + .stderr(""); +} + +fn val(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "val") + .assert() + .success() + .stdout("null\n") + .stderr(""); +} + +async fn i32(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "i32_", 42).await; +} + +async fn i64(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "i64_", i64::MAX).await; +} + +async fn negative_i32(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "i32_", -42).await; +} + +async fn negative_i64(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "i64_", i64::MIN).await; +} + +async fn account_address(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "addresse", + json!("GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS"), + ) + .await; +} + +async fn contract_address(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip( + sandbox, + id, + "addresse", + json!("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"), + ) + .await; +} + +async fn bytes(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "bytes", json!("7374656c6c6172")).await; +} + +async fn const_enum(sandbox: &TestEnv, id: &str) { + invoke_with_roundtrip(sandbox, id, "card", "11").await; +} + +fn parse_u128(sandbox: &TestEnv, id: &str) { + let num = "340000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "u128") + .arg("--u128") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn parse_i128(sandbox: &TestEnv, id: &str) { + let num = "170000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "i128") + .arg("--i128") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn parse_negative_i128(sandbox: &TestEnv, id: &str) { + let num = "-170000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "i128") + .arg("--i128") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn parse_u256(sandbox: &TestEnv, id: &str) { + let num = "340000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "u256") + .arg("--u256") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn parse_i256(sandbox: &TestEnv, id: &str) { + let num = "170000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "i256") + .arg("--i256") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn parse_negative_i256(sandbox: &TestEnv, id: &str) { + let num = "-170000000000000000000000000000000000000"; + invoke_custom(sandbox, id, "i256") + .arg("--i256") + .arg(num) + .assert() + .success() + .stdout(format!( + r#""{num}" +"#, + )); +} + +fn boolean(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "boolean") + .arg("--boolean") + .assert() + .success() + .stdout( + r#"true +"#, + ); +} +fn boolean_two(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "boolean") + .arg("--boolean") + .arg("true") + .assert() + .success() + .stdout( + r#"true +"#, + ); +} + +fn boolean_no_flag(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "boolean") + .assert() + .success() + .stdout( + r#"false +"#, + ); +} + +fn boolean_false(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "boolean") + .arg("--boolean") + .arg("false") + .assert() + .success() + .stdout( + r#"false +"#, + ); +} + +fn boolean_not(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "not") + .arg("--boolean") + .assert() + .success() + .stdout( + r#"false +"#, + ); +} + +fn boolean_not_no_flag(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "not").assert().success().stdout( + r#"true +"#, + ); +} + +fn option_none(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "option") + .assert() + .success() + .stdout( + r#"null +"#, + ); +} + +fn option_some(sandbox: &TestEnv, id: &str) { + invoke_custom(sandbox, id, "option") + .arg("--option=1") + .assert() + .success() + .stdout( + r#"1 +"#, + ); +} diff --git a/cmd/crates/soroban-test/tests/it/dotenv.rs b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs similarity index 63% rename from cmd/crates/soroban-test/tests/it/dotenv.rs rename to cmd/crates/soroban-test/tests/it/integration/dotenv.rs index eb7b5a68..d7d56aaf 100644 --- a/cmd/crates/soroban-test/tests/it/dotenv.rs +++ b/cmd/crates/soroban-test/tests/it/integration/dotenv.rs @@ -1,19 +1,6 @@ use soroban_test::TestEnv; -use crate::util::HELLO_WORLD; - -const SOROBAN_CONTRACT_ID: &str = "SOROBAN_CONTRACT_ID=1"; - -fn deploy(e: &TestEnv, id: &str) { - e.new_assert_cmd("contract") - .arg("deploy") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--id") - .arg(id) - .assert() - .success(); -} +use super::util::{deploy_hello, TEST_CONTRACT_ID}; fn write_env_file(e: &TestEnv, contents: &str) { let env_file = e.dir().join(".env"); @@ -21,11 +8,15 @@ fn write_env_file(e: &TestEnv, contents: &str) { assert_eq!(contents, std::fs::read_to_string(env_file).unwrap()); } +fn contract_id() -> String { + format!("SOROBAN_CONTRACT_ID={TEST_CONTRACT_ID}") +} + #[test] fn can_read_file() { TestEnv::with_default(|e| { - deploy(e, "1"); - write_env_file(e, SOROBAN_CONTRACT_ID); + deploy_hello(e); + write_env_file(e, &contract_id()); e.new_assert_cmd("contract") .arg("invoke") .arg("--") @@ -40,8 +31,8 @@ fn can_read_file() { #[test] fn current_env_not_overwritten() { TestEnv::with_default(|e| { - deploy(e, "1"); - write_env_file(e, SOROBAN_CONTRACT_ID); + deploy_hello(e); + write_env_file(e, &contract_id()); e.new_assert_cmd("contract") .env("SOROBAN_CONTRACT_ID", "2") @@ -50,20 +41,20 @@ fn current_env_not_overwritten() { .arg("hello") .arg("--world=world") .assert() - .stderr("error: parsing contract spec: contract spec not found\n"); + .stderr("error: Contract not found: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4\n"); }); } #[test] fn cli_args_have_priority() { TestEnv::with_default(|e| { - deploy(e, "2"); - write_env_file(e, SOROBAN_CONTRACT_ID); + deploy_hello(e); + write_env_file(e, &contract_id()); e.new_assert_cmd("contract") - .env("SOROBAN_CONTRACT_ID", "3") + .env("SOROBAN_CONTRACT_ID", "2") .arg("invoke") .arg("--id") - .arg("2") + .arg(TEST_CONTRACT_ID) .arg("--") .arg("hello") .arg("--world=world") diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs new file mode 100644 index 00000000..1b77da22 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -0,0 +1,290 @@ +use soroban_cli::commands::{ + config::identity, + contract::{self, fetch}, +}; +use soroban_test::TestEnv; + +use crate::{integration::util::bump_contract, util::DEFAULT_SEED_PHRASE}; + +use super::util::{ + add_test_seed, bump, deploy_hello, network_passphrase, network_passphrase_arg, rpc_url, + rpc_url_arg, DEFAULT_PUB_KEY, DEFAULT_PUB_KEY_1, DEFAULT_SECRET_KEY, HELLO_WORLD, +}; + +#[tokio::test] +#[ignore] +async fn invoke() { + let sandbox = &TestEnv::default(); + let id = &deploy_hello(sandbox); + bump_contract(sandbox, id, HELLO_WORLD).await; + // Note that all functions tested here have no state + invoke_hello_world(sandbox, id); + invoke_hello_world_with_lib(sandbox, id).await; + 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; + // 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; + // This does add an identity to local config + invoke_with_id(sandbox, id).await; + handles_kebab_case(sandbox, id).await; + fetch(sandbox, id).await; + invoke_prng_u64_in_range_test(sandbox, id).await; +} + +fn invoke_hello_world(sandbox: &TestEnv, id: &str) { + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--") + .arg("hello") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); +} + +async fn invoke_hello_world_with_lib(e: &TestEnv, id: &str) { + let mut 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(); + 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) { + 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("--world=world") + .assert() + .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .success(); + + // Invoke it again without providing the contract, to exercise the deployment + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--") + .arg("auth") + .arg(&format!("--addr={DEFAULT_PUB_KEY}")) + .arg("--world=world") + .assert() + .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) + .success(); +} + +async fn invoke_auth_with_identity(sandbox: &TestEnv, id: &str) { + sandbox + .cmd::("test -d ") + .run() + .await + .unwrap(); + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--wasm") + .arg(HELLO_WORLD.path()) + .arg("--") + .arg("auth") + .arg("--addr") + .arg(DEFAULT_PUB_KEY) + .arg("--world=world") + .assert() + .stdout(format!("\"{DEFAULT_PUB_KEY}\"\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) { + let res = sandbox + .invoke(&[ + "--hd-path=0", + "--id", + id, + &rpc_url_arg().unwrap_or_default(), + &network_passphrase_arg().unwrap_or_default(), + "--", + "auth", + &format!("--addr={DEFAULT_PUB_KEY_1}"), + "--world=world", + ]) + .await; + let e = res.unwrap_err(); + assert!( + matches!(e, contract::invoke::Error::Rpc(_)), + "Expected rpc error got {e:?}" + ); +} + +fn contract_data_read_failure(sandbox: &TestEnv, id: &str) { + sandbox + .new_assert_cmd("contract") + .arg("read") + .arg("--id") + .arg(id) + .arg("--key=COUNTER") + .arg("--durability=persistent") + .assert() + .failure() + .stderr( + "error: no matching contract data entries were found for the specified contract id\n", + ); +} + +#[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(); + assert_eq!(res.trim(), "1"); + bump(sandbox, id, Some(KEY)).await; + + sandbox + .new_assert_cmd("contract") + .arg("read") + .arg("--id") + .arg(id) + .arg("--key") + .arg(KEY) + .arg("--durability=persistent") + .assert() + .success() + .stdout(predicates::str::starts_with("COUNTER,1")); + + sandbox + .new_assert_cmd("contract") + .arg("invoke") + .arg("--id") + .arg(id) + .arg("--") + .arg("inc") + .assert() + .success(); + + sandbox + .new_assert_cmd("contract") + .arg("read") + .arg("--id") + .arg(id) + .arg("--key") + .arg(KEY) + .arg("--durability=persistent") + .assert() + .success() + .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; +} + +async fn invoke_with_sk(sandbox: &TestEnv, id: &str) { + invoke_with_source(sandbox, DEFAULT_SECRET_KEY, 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; +} + +async fn invoke_with_source(sandbox: &TestEnv, source: &str, id: &str) { + let cmd = sandbox + .invoke(&[ + "--source-account", + source, + "--id", + id, + "--", + "hello", + "--world=world", + ]) + .await + .unwrap(); + assert_eq!(cmd, "[\"Hello\",\"world\"]"); +} + +async fn handles_kebab_case(e: &TestEnv, id: &str) { + assert!(e + .invoke(&["--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()]); + cmd.run().await.unwrap(); + assert!(f.exists()); +} + +async fn invoke_prng_u64_in_range_test(sandbox: &TestEnv, id: &str) { + assert!(sandbox + .invoke(&[ + "--id", + id, + "--wasm", + HELLO_WORLD.path().to_str().unwrap(), + "--", + "prng_u64_in_range", + "--low=0", + "--high=100", + ]) + .await + .is_ok()); +} diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs new file mode 100644 index 00000000..951f2034 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -0,0 +1,117 @@ +use soroban_cli::commands::contract; +use soroban_test::{TestEnv, Wasm}; +use std::{fmt::Display, path::Path}; + +use crate::util::{add_identity, SecretKind}; + +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_identity( + 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, +{ + let data = data.to_string(); + println!("{data}"); + let res = e + .invoke(&["--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 fn deploy_hello(sandbox: &TestEnv) -> String { + deploy_contract(sandbox, HELLO_WORLD) +} + +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()) + .assert() + .success() + .stdout(format!("{hash}\n")); + + sandbox + .new_assert_cmd("contract") + .arg("deploy") + .arg("--wasm-hash") + .arg(&format!("{hash}")) + .arg("--salt") + .arg(TEST_SALT) + .assert() + .success() + .stdout(format!("{TEST_CONTRACT_ID}\n")); + TEST_CONTRACT_ID.to_string() +} + +pub async fn bump_contract(sandbox: &TestEnv, id: &str, wasm: &Wasm<'_>) { + bump(sandbox, id, None).await; + let cmd: contract::bump::Cmd = sandbox.cmd_arr(&[ + "--wasm-hash", + wasm.hash().unwrap().to_string().as_str(), + "--durability", + "persistent", + "--ledgers-to-expire", + "100000", + ]); + cmd.run().await.unwrap(); +} + +pub async fn bump(sandbox: &TestEnv, id: &str, value: Option<&str>) { + let mut args = vec![ + "--id", + id, + "--durability", + "persistent", + "--ledgers-to-expire", + "100000", + ]; + if let Some(value) = value { + args.push("--key"); + args.push(value); + } + let cmd: contract::bump::Cmd = sandbox.cmd_arr(&args); + cmd.run().await.unwrap(); +} diff --git a/cmd/crates/soroban-test/tests/it/integration_and_sandbox.rs b/cmd/crates/soroban-test/tests/it/integration_and_sandbox.rs deleted file mode 100644 index 5536966c..00000000 --- a/cmd/crates/soroban-test/tests/it/integration_and_sandbox.rs +++ /dev/null @@ -1,222 +0,0 @@ -use soroban_cli::commands::{config::identity, contract::fetch}; -use soroban_test::TestEnv; - -use crate::util::{ - add_test_seed, deploy_hello, is_rpc, network_passphrase_arg, rpc_url_arg, DEFAULT_PUB_KEY, - DEFAULT_SECRET_KEY, DEFAULT_SEED_PHRASE, HELLO_WORLD, -}; - -#[test] -fn invoke_hello_world_with_lib_two() { - TestEnv::with_default(|e| { - let id = deploy_hello(e); - 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()).unwrap() - } else { - e.invoke(&[invoke_args, args].concat()).unwrap() - }; - assert_eq!(res, r#"["Hello","world"]"#); - }); -} - -#[test] -fn invoke_auth() { - let sandbox = TestEnv::default(); - let id = &deploy_hello(&sandbox); - 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("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); - - // Invoke it again without providing the contract, to exercise the deployment - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("auth") - .arg(&format!("--addr={DEFAULT_PUB_KEY}")) - .arg("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); -} - -#[tokio::test] -async fn invoke_auth_with_identity() { - let sandbox = TestEnv::default(); - sandbox - .cmd::("test -d ") - .run() - .await - .unwrap(); - let id = deploy_hello(&sandbox); - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .arg("--") - .arg("auth") - .arg("--addr=test") - .arg("--world=world") - .assert() - .stdout(format!("\"{DEFAULT_PUB_KEY}\"\n")) - .success(); -} - -#[test] -fn contract_data_read_failure() { - let sandbox = TestEnv::default(); - let id = deploy_hello(&sandbox); - - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .failure() - .stderr( - "error: no matching contract data entries were found for the specified contract id\n", - ); -} - -#[test] -fn contract_data_read() { - let sandbox = TestEnv::default(); - let id = &deploy_hello(&sandbox); - - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("inc") - .assert() - .success(); - - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .success() - .stdout(predicates::str::starts_with("COUNTER,1")); - - sandbox - .new_assert_cmd("contract") - .arg("invoke") - .arg("--id") - .arg(id) - .arg("--") - .arg("inc") - .assert() - .success(); - - sandbox - .new_assert_cmd("contract") - .arg("read") - .arg("--id") - .arg(id) - .arg("--key=COUNTER") - .arg("--durability=persistent") - .assert() - .success() - .stdout(predicates::str::starts_with("COUNTER,2")); -} - -#[test] -fn invoke_hello_world_with_seed() { - let sandbox = TestEnv::default(); - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(&sandbox, &identity); -} - -#[test] -fn invoke_with_seed() { - let sandbox = TestEnv::default(); - invoke_with_source(&sandbox, DEFAULT_SEED_PHRASE); -} - -#[test] -fn invoke_with_id() { - let sandbox = TestEnv::default(); - let identity = add_test_seed(sandbox.dir()); - invoke_with_source(&sandbox, &identity); -} - -#[test] -fn invoke_with_sk() { - let sandbox = TestEnv::default(); - invoke_with_source(&sandbox, DEFAULT_SECRET_KEY); -} - -fn invoke_with_source(sandbox: &TestEnv, source: &str) { - let id = &deploy_hello(sandbox); - let cmd = sandbox.invoke(&[ - "--source-account", - source, - "--id", - id, - "--wasm", - HELLO_WORLD.path().to_str().unwrap(), - &rpc_url_arg().unwrap_or_default(), - &network_passphrase_arg().unwrap_or_default(), - "--", - "hello", - "--world=world", - ]); - assert_eq!(cmd.unwrap(), "[\"Hello\",\"world\"]"); - - // Invoke it again without providing the contract, to exercise the deployment - let cmd = sandbox.invoke(&[ - "--source-account", - source, - "--id", - id, - "--", - "hello", - "--world=world", - ]); - assert_eq!(cmd.unwrap(), "[\"Hello\",\"world\"]"); -} - -#[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/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index e822b774..3077d2fa 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -1,10 +1,8 @@ mod arg_parsing; mod config; - -mod custom_types; -mod dotenv; -mod hello_world; -mod integration_and_sandbox; +mod help; +#[cfg(feature = "integration")] +mod integration; mod lab; 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 68c2e31a..ad5dacc7 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -1,10 +1,11 @@ -use std::{fmt::Display, path::Path}; +use std::path::Path; -use assert_cmd::Command; -use soroban_cli::commands::config::{locator::KeyType, secret::Secret}; +use soroban_cli::commands::{ + config::{locator::KeyType, secret::Secret}, + contract, +}; use soroban_test::{TestEnv, Wasm}; -pub const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world"); pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types"); #[derive(Clone)] @@ -40,102 +41,20 @@ pub fn add_test_id(dir: &Path) -> String { name.to_owned() } -pub fn add_test_seed(dir: &Path) -> String { - let name = "test_seed"; - add_identity( - dir, - name, - SecretKind::Seed, - "coral light army gather adapt blossom school alcohol coral light army giggle", - ); - name.to_owned() -} - -pub fn invoke(sandbox: &TestEnv, func: &str) -> Command { - let mut s = sandbox.new_assert_cmd("contract"); - s.arg("invoke") - .arg("--id=1") - .arg("--wasm") - .arg(CUSTOM_TYPES.path()) - .arg("--") - .arg(func); - s -} - -pub fn invoke_with_roundtrip(func: &str, data: D) -where - D: Display, -{ - TestEnv::with_default(|e| { - let data = data.to_string(); - println!("{data}"); - let res = e - .invoke(&[ - "--id=1", - "--wasm", - &CUSTOM_TYPES.to_string(), - "--", - func, - &format!("--{func}"), - &data, - ]) - .unwrap(); - assert_eq!(res, data); - }); -} - -pub fn is_rpc() -> bool { - std::env::var("SOROBAN_RPC_URL").is_ok() -} - pub const DEFAULT_SEED_PHRASE: &str = "coral light army gather adapt blossom school alcohol coral light army giggle"; -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 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 const TEST_CONTRACT_ID: &str = "CBVTIVBYWAO2HNPNGKDCZW4OZYYESTKNGD7IPRTDGQSFJS4QBDQQJX3T"; - -pub fn deploy_hello(sandbox: &TestEnv) -> String { - let hash = HELLO_WORLD.hash().unwrap(); - sandbox - .new_assert_cmd("contract") - .arg("install") - .arg("--wasm") - .arg(HELLO_WORLD.path()) - .assert() - .success() - .stdout(format!("{hash}\n")); - - let mut cmd: &mut assert_cmd::Command = &mut sandbox.new_assert_cmd("contract"); - - cmd = cmd.arg("deploy").arg("--wasm-hash").arg(&format!("{hash}")); - if is_rpc() { - cmd = cmd.arg("--salt").arg(TEST_SALT); - } else { - cmd = cmd.arg("--id").arg(TEST_CONTRACT_ID); - } - cmd.assert() - .success() - .stdout(format!("{TEST_CONTRACT_ID}\n")); - TEST_CONTRACT_ID.to_string() +#[allow(dead_code)] +pub async fn invoke_custom( + sandbox: &TestEnv, + id: &str, + func: &str, + arg: &str, + wasm: &Path, +) -> Result { + let mut i: contract::invoke::Cmd = sandbox.cmd_arr(&["--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 } diff --git a/cmd/soroban-cli/src/commands/config/events_file.rs b/cmd/soroban-cli/src/commands/config/events_file.rs deleted file mode 100644 index ba1b280b..00000000 --- a/cmd/soroban-cli/src/commands/config/events_file.rs +++ /dev/null @@ -1,236 +0,0 @@ -use crate::{ - commands::HEADING_SANDBOX, - rpc::{self, does_topic_match, Event}, - toid, -}; -use chrono::{DateTime, NaiveDateTime, Utc}; -use clap::arg; -use soroban_env_host::{ - events, - xdr::{self, WriteXdr}, -}; -use soroban_ledger_snapshot::LedgerSnapshot; -use std::{ - fs, - path::{Path, PathBuf}, -}; -use stellar_strkey::{Contract, Strkey}; - -#[derive(Debug, clap::Args, Clone, Default)] -#[group(skip)] -pub struct Args { - /// File to persist events, default is `.soroban/events.json` - #[arg( - long, - value_name = "PATH", - env = "SOROBAN_EVENTS_FILE", - help_heading = HEADING_SANDBOX, - conflicts_with = "rpc_url", - conflicts_with = "network", - )] - pub events_file: Option, -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error(transparent)] - Xdr(#[from] xdr::Error), - #[error(transparent)] - Rpc(#[from] rpc::Error), - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Generic(#[from] Box), - #[error("invalid timestamp in event: {ts}")] - InvalidTimestamp { ts: String }, -} - -impl Args { - /// Returns a list of events from the on-disk event store, which stores events - /// exactly as they'd be returned by an RPC server. - pub fn read(&self, pwd: &Path) -> Result { - let path = self.path(pwd); - let reader = std::fs::OpenOptions::new().read(true).open(path)?; - Ok(serde_json::from_reader(reader)?) - } - - /// Reads the existing event file, appends the new events, and writes it all to - /// disk. Note that this almost certainly isn't safe to call in parallel. - pub fn commit( - &self, - new_events: &[events::HostEvent], - ledger_info: &LedgerSnapshot, - pwd: &Path, - ) -> Result<(), Error> { - let output_file = self.path(pwd); - // Create the directory tree if necessary, since these are unlikely to be - // the first events. - if let Some(dir) = output_file.parent() { - if !dir.exists() { - fs::create_dir_all(dir)?; - } - } - - let mut events: Vec = if output_file.exists() { - let mut file = fs::OpenOptions::new().read(true).open(&output_file)?; - let payload: rpc::GetEventsResponse = serde_json::from_reader(&mut file)?; - payload.events - } else { - vec![] - }; - - for (i, event) in new_events.iter().enumerate() { - let contract_event = &event.event; - let topic = match &contract_event.body { - xdr::ContractEventBody::V0(e) => &e.topics, - } - .iter() - .map(xdr::WriteXdr::to_xdr_base64) - .collect::, _>>()?; - - // stolen from - // https://github.com/stellar/soroban-tools/blob/main/cmd/soroban-rpc/internal/methods/get_events.go#L264 - let id = format!( - "{}-{:010}", - toid::Toid::new( - ledger_info.sequence_number, - // we should technically inject the tx order here from the - // ledger info, but the sandbox does one tx/op per ledger - // anyway, so this is a safe assumption - 1, - 1, - ) - .to_paging_token(), - i + 1 - ); - - // Misc. timestamp to RFC 3339-formatted datetime nonsense, with an - // absurd amount of verbosity because every edge case needs its own - // chain of error-handling methods. - // - // Reference: https://stackoverflow.com/a/50072164 - let ts: i64 = - ledger_info - .timestamp - .try_into() - .map_err(|_e| Error::InvalidTimestamp { - ts: ledger_info.timestamp.to_string(), - })?; - let ndt = NaiveDateTime::from_timestamp_opt(ts, 0).ok_or_else(|| { - Error::InvalidTimestamp { - ts: ledger_info.timestamp.to_string(), - } - })?; - - let dt: DateTime = DateTime::from_naive_utc_and_offset(ndt, Utc); - - let cereal_event = rpc::Event { - event_type: match contract_event.type_ { - xdr::ContractEventType::Contract => "contract", - xdr::ContractEventType::System => "system", - xdr::ContractEventType::Diagnostic => "diagnostic", - } - .to_string(), - paging_token: id.clone(), - id, - ledger: ledger_info.sequence_number.to_string(), - ledger_closed_at: dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(), - contract_id: Strkey::Contract(Contract( - contract_event - .contract_id - .as_ref() - .unwrap_or(&xdr::Hash([0; 32])) - .0, - )) - .to_string(), - topic, - value: match &contract_event.body { - xdr::ContractEventBody::V0(e) => &e.data, - } - .to_xdr_base64()?, - }; - - events.push(cereal_event); - } - - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&output_file)?; - - serde_json::to_writer_pretty( - &mut file, - &rpc::GetEventsResponse { - events, - latest_ledger: ledger_info.sequence_number, - }, - )?; - - Ok(()) - } - - pub fn path(&self, pwd: &Path) -> PathBuf { - if let Some(path) = &self.events_file { - path.clone() - } else { - pwd.join("events.json") - } - } - - pub fn filter_events( - events: &[Event], - path: &Path, - start_cursor: (u64, i32), - contract_ids: &[String], - topic_filters: &[String], - count: usize, - ) -> Vec { - events - .iter() - .filter(|evt| match evt.parse_cursor() { - Ok(event_cursor) => event_cursor > start_cursor, - Err(e) => { - eprintln!("error parsing key 'ledger': {e:?}"); - eprintln!( - "your sandbox events file ('{path:?}') may be corrupt, consider deleting it", - ); - eprintln!("ignoring this event: {evt:#?}"); - - false - } - }) - .filter(|evt| { - // Contract ID filter(s) are optional, so we should render all - // events if they're omitted. - contract_ids.is_empty() || contract_ids.iter().any(|id| *id == evt.contract_id) - }) - .filter(|evt| { - // Like before, no topic filters means pass everything through. - topic_filters.is_empty() || - // Reminder: All of the topic filters are part of a single - // filter object, and each one contains segments, so we need to - // apply all of them to the given event. - topic_filters - .iter() - // quadratic, but both are <= 5 long - .any(|f| { - does_topic_match( - &evt.topic, - // misc. Rust nonsense: make a copy over the given - // split filter, because passing a slice of - // references is too much for this language to - // handle - &f.split(',') - .map(std::string::ToString::to_string) - .collect::>() - ) - }) - }) - .take(count) - .cloned() - .collect() - } -} diff --git a/cmd/soroban-cli/src/commands/config/ledger_file.rs b/cmd/soroban-cli/src/commands/config/ledger_file.rs deleted file mode 100644 index f85cbfda..00000000 --- a/cmd/soroban-cli/src/commands/config/ledger_file.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::{commands::HEADING_SANDBOX, utils}; -use clap::arg; -use soroban_ledger_snapshot::LedgerSnapshot; -use std::path::{Path, PathBuf}; - -#[derive(Debug, clap::Args, Clone, Default)] -#[group(skip)] -pub struct Args { - /// File to persist ledger state, default is `.soroban/ledger.json` - #[arg( - long, - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - pub ledger_file: Option, -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: PathBuf, - error: soroban_ledger_snapshot::Error, - }, - - #[error("committing file {filepath}: {error}")] - CannotCommitLedgerFile { - filepath: PathBuf, - error: soroban_ledger_snapshot::Error, - }, -} - -impl Args { - pub fn read(&self, pwd: &Path) -> Result { - let filepath = self.path(pwd); - utils::ledger_snapshot_read_or_default(&filepath) - .map_err(|e| Error::CannotReadLedgerFile { filepath, error: e }) - } - - pub fn write(&self, state: &LedgerSnapshot, pwd: &Path) -> Result<(), Error> { - let filepath = self.path(pwd); - - state - .write_file(&filepath) - .map_err(|e| Error::CannotCommitLedgerFile { filepath, error: e }) - } - - pub fn path(&self, pwd: &Path) -> PathBuf { - if let Some(path) = &self.ledger_file { - path.clone() - } else { - pwd.join("ledger.json") - } - } -} diff --git a/cmd/soroban-cli/src/commands/config/mod.rs b/cmd/soroban-cli/src/commands/config/mod.rs index 8b47b32b..f4d8ec55 100644 --- a/cmd/soroban-cli/src/commands/config/mod.rs +++ b/cmd/soroban-cli/src/commands/config/mod.rs @@ -2,15 +2,12 @@ use std::path::PathBuf; use clap::{arg, command, Parser}; use serde::{Deserialize, Serialize}; -use soroban_ledger_snapshot::LedgerSnapshot; use crate::Pwd; use self::{network::Network, secret::Secret}; -pub mod events_file; pub mod identity; -pub mod ledger_file; pub mod locator; pub mod network; pub mod secret; @@ -20,7 +17,6 @@ pub enum Cmd { /// Configure different identities to sign transactions. #[command(subcommand)] Identity(identity::Cmd), - /// Configure different networks #[command(subcommand)] Network(network::Cmd), @@ -30,16 +26,10 @@ pub enum Cmd { pub enum Error { #[error(transparent)] Identity(#[from] identity::Error), - #[error(transparent)] Network(#[from] network::Error), - - #[error(transparent)] - Ledger(#[from] ledger_file::Error), - #[error(transparent)] Secret(#[from] secret::Error), - #[error(transparent)] Config(#[from] locator::Error), } @@ -60,9 +50,6 @@ pub struct Args { #[command(flatten)] pub network: network::Args, - #[command(flatten)] - pub ledger_file: ledger_file::Args, - #[arg(long, visible_alias = "source", env = "SOROBAN_ACCOUNT")] /// Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` pub source_account: Option, @@ -98,18 +85,6 @@ impl Args { Ok(self.network.get(&self.locator)?) } - pub fn is_no_network(&self) -> bool { - self.network.is_no_network() - } - - pub fn get_state(&self) -> Result { - Ok(self.ledger_file.read(&self.locator.config_dir()?)?) - } - - pub fn set_state(&self, state: &LedgerSnapshot) -> Result<(), Error> { - Ok(self.ledger_file.write(state, &self.locator.config_dir()?)?) - } - pub fn config_dir(&self) -> Result { Ok(self.locator.config_dir()?) } diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index ef5856ea..c0a78015 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -7,7 +7,7 @@ use crate::wasm; use crate::{ commands::{ config::{ - ledger_file, locator, + locator, network::{self, Network}, }, contract::{self, fetch}, @@ -21,22 +21,17 @@ pub struct Cmd { /// Path to optional wasm binary #[arg(long)] pub wasm: Option, - /// Where to place generated project #[arg(long)] output_dir: PathBuf, - /// Whether to overwrite output directory if it already exists #[arg(long)] overwrite: bool, - /// The contract ID/address on the network #[arg(long, visible_alias = "id")] contract_id: String, - #[command(flatten)] locator: locator::Args, - #[command(flatten)] network: network::Args, } @@ -79,7 +74,6 @@ impl Cmd { out_file: None, locator: self.locator.clone(), network: self.network.clone(), - ledger_file: ledger_file::Args::default(), }; let bytes = fetch.get_bytes().await?; ContractSpec::new(&bytes)?.spec diff --git a/cmd/soroban-cli/src/commands/contract/bump.rs b/cmd/soroban-cli/src/commands/contract/bump.rs index 06a86cf9..8d3b3337 100644 --- a/cmd/soroban-cli/src/commands/contract/bump.rs +++ b/cmd/soroban-cli/src/commands/contract/bump.rs @@ -83,11 +83,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = if self.config.is_no_network() { - self.run_in_sandbox()? - } else { - self.run_against_rpc_server().await? - }; + let expiration_ledger_seq = self.run_against_rpc_server().await?; if self.expiration_ledger_only { println!("{expiration_ledger_seq}"); } else { @@ -196,46 +192,4 @@ impl Cmd { _ => Err(Error::LedgerEntryNotFound), } } - - fn run_in_sandbox(&self) -> Result { - let keys = self.key.parse_keys()?; - - // Initialize storage and host - // TODO: allow option to separate input and output file - let mut state = self.config.get_state()?; - - // Update all matching entries - let mut expiration_ledger_seq = None; - state.ledger_entries = state - .ledger_entries - .iter() - .map(|(k, v)| { - let new_k = k.as_ref().clone(); - let new_v = v.0.as_ref().clone(); - let new_e = v.1; - ( - Box::new(new_k.clone()), - ( - Box::new(new_v), - if keys.contains(&new_k) { - // It must have an expiration since it's a contract data entry - let old_expiration = v.1.unwrap(); - expiration_ledger_seq = Some(old_expiration + self.ledgers_to_expire); - expiration_ledger_seq - } else { - new_e - }, - ), - ) - }) - .collect::>(); - - self.config.set_state(&state)?; - - let Some(new_expiration_ledger_seq) = expiration_ledger_seq else { - return Err(Error::LedgerEntryNotFound); - }; - - Ok(new_expiration_ledger_seq) - } } diff --git a/cmd/soroban-cli/src/commands/contract/deploy.rs b/cmd/soroban-cli/src/commands/contract/deploy.rs index 7c7152bc..2b3cffb1 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy.rs @@ -17,7 +17,7 @@ use soroban_env_host::{ }; use crate::{ - commands::{config, contract::install, HEADING_RPC, HEADING_SANDBOX}, + commands::{config, contract::install, HEADING_RPC}, rpc::{self, Client}, utils, wasm, }; @@ -33,22 +33,12 @@ pub struct Cmd { /// WASM file to deploy #[arg(long, group = "wasm_src")] wasm: Option, - /// Hash of the already installed/deployed WASM file #[arg(long = "wasm-hash", conflicts_with = "wasm", group = "wasm_src")] wasm_hash: Option, - - /// Contract ID to deploy to - #[arg( - long = "id", - conflicts_with = "rpc_url", - help_heading = HEADING_SANDBOX, - )] - contract_id: Option, /// Custom salt 32-byte salt for the token id #[arg( long, - conflicts_with_all = &["contract_id", "ledger_file"], help_heading = HEADING_RPC, )] salt: Option, @@ -125,34 +115,7 @@ impl Cmd { } })?); - if self.config.is_no_network() { - self.run_in_sandbox(hash) - } else { - self.run_against_rpc_server(hash).await - } - } - - #[allow(clippy::needless_pass_by_value)] - pub fn run_in_sandbox(&self, wasm_hash: Hash) -> Result { - let contract_id: [u8; 32] = match &self.contract_id { - Some(id) => { - utils::contract_id_from_str(id).map_err(|e| Error::CannotParseContractId { - contract_id: self.contract_id.as_ref().unwrap().clone(), - error: e, - })? - } - None => rand::thread_rng().gen::<[u8; 32]>(), - }; - - let mut state = self.config.get_state()?; - utils::add_contract_to_ledger_entries( - &mut state.ledger_entries, - contract_id, - wasm_hash.0, - state.min_persistent_entry_expiration, - ); - self.config.set_state(&state)?; - Ok(stellar_strkey::Contract(contract_id).to_string()) + self.run_against_rpc_server(hash).await } async fn run_against_rpc_server(&self, wasm_hash: Hash) -> Result { diff --git a/cmd/soroban-cli/src/commands/contract/fetch.rs b/cmd/soroban-cli/src/commands/contract/fetch.rs index a1284123..9097ffde 100644 --- a/cmd/soroban-cli/src/commands/contract/fetch.rs +++ b/cmd/soroban-cli/src/commands/contract/fetch.rs @@ -3,7 +3,7 @@ use std::convert::Infallible; use std::io::Write; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::{fmt::Debug, fs, io, rc::Rc}; +use std::{fmt::Debug, fs, io}; use clap::{arg, command, Parser}; use soroban_env_host::{ @@ -16,12 +16,10 @@ use soroban_env_host::{ }, }; -use soroban_ledger_snapshot::LedgerSnapshot; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; use super::super::config::{self, locator}; -use crate::commands::config::ledger_file; use crate::commands::config::network::{self, Network}; use crate::{ rpc::{self, Client}, @@ -42,8 +40,6 @@ pub struct Cmd { pub locator: locator::Args, #[command(flatten)] pub network: network::Args, - #[command(flatten)] - pub ledger_file: ledger_file::Args, } impl FromStr for Cmd { @@ -87,8 +83,6 @@ pub enum Error { NetworkNotProvided, #[error(transparent)] Network(#[from] network::Error), - #[error(transparent)] - Ledger(#[from] ledger_file::Error), #[error("cannot create contract directory for {0:?}")] CannotCreateContractDir(PathBuf), } @@ -121,11 +115,7 @@ impl Cmd { } pub async fn get_bytes(&self) -> Result, Error> { - if self.network.is_no_network() { - self.run_in_sandbox() - } else { - self.run_against_rpc_server().await - } + self.run_against_rpc_server().await } pub fn network(&self) -> Result { @@ -144,18 +134,6 @@ impl Cmd { Ok(client.get_remote_wasm(&contract_id).await?) } - pub fn get_state(&self) -> Result { - Ok(self.ledger_file.read(&self.locator.config_dir()?)?) - } - - pub fn run_in_sandbox(&self) -> Result, Error> { - let contract_id = self.contract_id()?; - // Initialize storage and host - let snap = Rc::new(self.get_state()?); - let mut storage = Storage::with_recording_footprint(snap); - Ok(get_contract_wasm_from_storage(&mut storage, contract_id)?) - } - 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)) diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 606383ea..05e867bc 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -55,28 +55,10 @@ impl Cmd { } pub async fn run_and_get_hash(&self) -> Result { - let contract = self.wasm.read()?; - if self.config.is_no_network() { - self.run_in_sandbox(contract) - } else { - self.run_against_rpc_server(contract).await - } - } - - pub fn run_in_sandbox(&self, contract: Vec) -> Result { - let mut state = self.config.get_state()?; - let wasm_hash = utils::add_contract_code_to_ledger_entries( - &mut state.ledger_entries, - contract, - state.min_persistent_entry_expiration, - )?; - - self.config.set_state(&state)?; - - Ok(wasm_hash) + self.run_against_rpc_server(&self.wasm.read()?).await } - async fn run_against_rpc_server(&self, contract: Vec) -> Result { + async fn run_against_rpc_server(&self, contract: &[u8]) -> Result { let network = self.config.get_network()?; let client = Client::new(&network.rpc_url)?; client @@ -91,7 +73,7 @@ impl Cmd { let sequence: i64 = account_details.seq_num.into(); let (tx_without_preflight, hash) = - build_install_contract_code_tx(contract.clone(), sequence + 1, self.fee.fee, &key)?; + build_install_contract_code_tx(contract, sequence + 1, self.fee.fee, &key)?; // Currently internal errors are not returned if the contract code is expired if let ( @@ -135,12 +117,12 @@ impl Cmd { } pub(crate) fn build_install_contract_code_tx( - source_code: Vec, + source_code: &[u8], sequence: i64, fee: u32, key: &ed25519_dalek::SigningKey, ) -> Result<(Transaction, Hash), XdrError> { - let hash = utils::contract_hash(&source_code)?; + let hash = utils::contract_hash(source_code)?; let op = Operation { source_account: Some(MuxedAccount::Ed25519(Uint256( @@ -172,7 +154,7 @@ mod tests { #[test] fn test_build_install_contract_code() { let result = build_install_contract_code_tx( - b"foo".to_vec(), + b"foo", 300, 1, &utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 8d83cff7..59c9bf44 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -4,38 +4,34 @@ use std::ffi::OsString; use std::num::ParseIntError; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::{fmt::Debug, fs, io, rc::Rc}; +use std::{fmt::Debug, fs, io}; use clap::{arg, command, value_parser, Parser}; use ed25519_dalek::SigningKey; use heck::ToKebabCase; -use soroban_env_host::e2e_invoke::{get_ledger_changes, ExpirationEntryMap}; -use soroban_env_host::xdr::ReadXdr; + use soroban_env_host::{ - budget::Budget, - storage::Storage, xdr::{ - self, AccountId, Error as XdrError, Hash, HostFunction, InvokeContractArgs, - InvokeHostFunctionOp, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, Memo, - MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, ScAddress, ScSpecEntry, - ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, SequenceNumber, SorobanAddressCredentials, - SorobanAuthorizationEntry, SorobanCredentials, SorobanResources, Transaction, - TransactionExt, Uint256, VecM, + self, Error as XdrError, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, + LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, + Preconditions, ScAddress, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec, + SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionExt, + Uint256, VecM, }, - DiagnosticLevel, Host, HostError, + HostError, }; use soroban_spec::read::FromWasmError; use stellar_strkey::DecodeError; use super::super::{ - config::{self, events_file, locator}, + config::{self, locator}, events, }; use crate::{ - commands::{global, HEADING_SANDBOX}, + commands::global, rpc::{self, Client}, - utils::{self, contract_spec, create_ledger_footprint, default_account_ledger_entry}, + utils::{self, contract_spec}, Pwd, }; use soroban_spec_tools::Spec; @@ -47,29 +43,18 @@ pub struct Cmd { /// Contract ID to invoke #[arg(long = "id", env = "SOROBAN_CONTRACT_ID")] pub contract_id: String, - /// WASM file of the contract to invoke (if using sandbox will deploy this file) - #[arg(long)] + // For testing only + #[arg(skip)] pub wasm: Option, - /// Output the cost execution to stderr - #[arg(long = "cost", conflicts_with = "rpc_url", conflicts_with="network", help_heading = HEADING_SANDBOX)] + #[arg(long = "cost")] pub cost: bool, - /// Run with an unlimited budget - #[arg(long = "unlimited-budget", - conflicts_with = "rpc_url", - conflicts_with = "network", - help_heading = HEADING_SANDBOX)] - pub unlimited_budget: bool, - /// Function name as subcommand, then arguments for that function as `--arg-name value` #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")] pub slop: Vec, - #[command(flatten)] pub config: config::Args, #[command(flatten)] - pub events_file: events_file::Args, - #[command(flatten)] pub fee: crate::fee::Args, } @@ -149,8 +134,6 @@ pub enum Error { #[error(transparent)] Clap(#[from] clap::Error), #[error(transparent)] - Events(#[from] events_file::Error), - #[error(transparent)] Locator(#[from] locator::Error), #[error("Contract Error\n{0}: {1}")] ContractInvoke(String, String), @@ -270,11 +253,7 @@ impl Cmd { } pub async fn invoke(&self, global_args: &global::Args) -> Result { - if self.config.is_no_network() { - self.run_in_sandbox(global_args) - } else { - self.run_against_rpc_server(global_args).await - } + self.run_against_rpc_server(global_args).await } pub async fn run_against_rpc_server( @@ -284,6 +263,11 @@ impl Cmd { let network = self.config.get_network()?; tracing::trace!(?network); let contract_id = self.contract_id()?; + let spec_entries = self.spec_entries()?; + if let Some(spec_entries) = &spec_entries { + // For testing wasm arg parsing + let _ = self.build_host_function_parameters(contract_id, spec_entries)?; + } let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) @@ -297,11 +281,7 @@ impl Cmd { let sequence: i64 = account_details.seq_num.into(); // Get the contract - let spec_entries = if let Some(spec) = self.spec_entries()? { - spec - } else { - client.get_remote_contract_spec(&contract_id).await? - }; + let spec_entries = client.get_remote_contract_spec(&contract_id).await?; // Get the ledger footprint let (function, spec, host_function_params, signers) = @@ -338,145 +318,6 @@ impl Cmd { output_to_string(&spec, &return_value, &function) } - pub fn run_in_sandbox(&self, global_args: &global::Args) -> Result { - let contract_id = self.contract_id()?; - // Initialize storage and host - // TODO: allow option to separate input and output file - let mut state = self.config.get_state()?; - - // If a file is specified, deploy the contract to storage - self.deploy_contract_in_sandbox(&mut state, &contract_id)?; - - // Create source account, adding it to the ledger if not already present. - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( - self.config.key_pair()?.verifying_key().to_bytes(), - ))); - let source_account_ledger_key = LedgerKey::Account(LedgerKeyAccount { - account_id: source_account.clone(), - }); - if !state - .ledger_entries - .iter() - .any(|(k, _)| **k == source_account_ledger_key) - { - state.ledger_entries.push(( - Box::new(source_account_ledger_key), - ( - Box::new(default_account_ledger_entry(source_account.clone())), - None, - ), - )); - } - - let snap = Rc::new(state.clone()); - let storage = Storage::with_recording_footprint(snap); - let spec_entries = if let Some(spec) = self.spec_entries()? { - spec - } else { - utils::get_contract_spec_from_state(&state, contract_id) - .map_err(Error::CannotParseContractSpec)? - }; - let budget = Budget::default(); - if self.unlimited_budget { - budget.reset_unlimited()?; - }; - let h = Host::with_storage_and_budget(storage, budget); - h.switch_to_recording_auth(true)?; - h.set_source_account(source_account)?; - h.set_base_prng_seed(rand::Rng::gen(&mut rand::thread_rng()))?; - - let mut ledger_info = state.ledger_info(); - ledger_info.sequence_number += 1; - ledger_info.timestamp += 5; - h.set_ledger_info(ledger_info.clone())?; - - let (function, spec, host_function_params, _signers) = - self.build_host_function_parameters(contract_id, &spec_entries)?; - h.set_diagnostic_level(DiagnosticLevel::Debug)?; - let resv = h - .invoke_function(HostFunction::InvokeContract(host_function_params)) - .map_err(|host_error| { - if let Ok(error) = spec.find_error_type(host_error.error.get_code()) { - Error::ContractInvoke(error.name.to_string_lossy(), error.doc.to_string_lossy()) - } else { - host_error.into() - } - })?; - - let res_str = output_to_string(&spec, &resv, &function)?; - - state.update(&h); - - let contract_auth: Vec = h - .get_recorded_auth_payloads()? - .into_iter() - .map(|payload| SorobanAuthorizationEntry { - credentials: match (payload.address, payload.nonce) { - (Some(address), Some(nonce)) => { - SorobanCredentials::Address(SorobanAddressCredentials { - address, - nonce, - signature_expiration_ledger: ledger_info.sequence_number + 1, - signature: ScVal::Void, - }) - } - _ => SorobanCredentials::SourceAccount, - }, - root_invocation: payload.invocation, - }) - .collect(); - let budget = h.budget_cloned(); - let (storage, events) = h.try_finish()?; - let footprint = &create_ledger_footprint(&storage.footprint); - - crate::log::host_events(&events.0); - log_events(footprint, &[contract_auth.try_into()?], &[]); - if global_args.verbose || global_args.very_verbose || self.cost { - log_budget(&budget); - } - - let ledger_changes = - get_ledger_changes(&budget, &storage, &state, ExpirationEntryMap::new())?; - let mut expiration_ledger_bumps: HashMap = HashMap::new(); - for ledger_entry_change in ledger_changes { - if let Some(exp_change) = ledger_entry_change.expiration_change { - let key = xdr::LedgerKey::from_xdr(ledger_entry_change.encoded_key)?; - expiration_ledger_bumps.insert(key, exp_change.new_expiration_ledger); - } - } - utils::bump_ledger_entry_expirations(&mut state.ledger_entries, &expiration_ledger_bumps); - - self.config.set_state(&state)?; - if !events.0.is_empty() { - self.events_file - .commit(&events.0, &state, &self.config.locator.config_dir()?)?; - } - Ok(res_str) - } - - pub fn deploy_contract_in_sandbox( - &self, - state: &mut soroban_ledger_snapshot::LedgerSnapshot, - contract_id: &[u8; 32], - ) -> Result<(), Error> { - if let Some(contract) = self.read_wasm()? { - let wasm_hash = utils::add_contract_code_to_ledger_entries( - &mut state.ledger_entries, - contract, - state.min_persistent_entry_expiration, - ) - .map_err(Error::CannotAddContractToLedgerEntries)? - .0; - utils::add_contract_to_ledger_entries( - &mut state.ledger_entries, - *contract_id, - wasm_hash, - state.min_persistent_entry_expiration, - ); - } - Ok(()) - } - 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))?) @@ -511,10 +352,6 @@ fn log_events( crate::log::footprint(footprint); } -fn log_budget(budget: &Budget) { - crate::log::budget(budget); -} - fn log_resources(resources: &SorobanResources) { crate::log::cost(resources); } diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index c2dc1a67..d9566b6c 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -28,7 +28,7 @@ pub enum Cmd { /// Deploy a contract Deploy(deploy::Cmd), - /// Fetch a contract's Wasm binary from a network or local sandbox + /// Fetch a contract's Wasm binary Fetch(fetch::Cmd), /// Inspect a WASM file listing contract functions, meta, etc diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 4c47f888..bf161f3f 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -90,11 +90,7 @@ pub enum Error { impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let entries = if self.config.is_no_network() { - self.run_in_sandbox()? - } else { - self.run_against_rpc_server().await? - }; + let entries = self.run_against_rpc_server().await?; self.output_entries(&entries) } @@ -107,31 +103,6 @@ impl Cmd { Ok(client.get_full_ledger_entries(&keys).await?) } - #[allow(clippy::too_many_lines)] - fn run_in_sandbox(&self) -> Result { - let state = self.config.get_state()?; - let ledger_entries = &state.ledger_entries; - let latest_ledger = u32::try_from(state.ledger_entries.len()).unwrap(); - let keys = self.key.parse_keys()?; - let entries = ledger_entries - .iter() - .map(|(k, v)| (k.as_ref().clone(), (v.0.as_ref().clone(), v.1))) - .filter(|(k, _v)| keys.contains(k)) - .map(|(key, (v, expiration))| { - Ok(FullLedgerEntry { - expiration_ledger_seq: expiration.unwrap_or_default(), - last_modified_ledger: latest_ledger, - key, - val: v.data, - }) - }) - .collect::, Error>>()?; - Ok(FullLedgerEntries { - entries, - latest_ledger: 0, - }) - } - fn output_entries(&self, entries: &FullLedgerEntries) -> Result<(), Error> { if entries.entries.is_empty() { return Err(Error::NoContractDataEntryFoundForContractID); diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index b6c8cd38..e709ebf7 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -84,11 +84,7 @@ pub enum Error { impl Cmd { #[allow(clippy::too_many_lines)] pub async fn run(&self) -> Result<(), Error> { - let expiration_ledger_seq = if self.config.is_no_network() { - self.run_in_sandbox()? - } else { - self.run_against_rpc_server().await? - }; + let expiration_ledger_seq = self.run_against_rpc_server().await?; if let Some(ledgers_to_expire) = self.ledgers_to_expire { bump::Cmd { @@ -180,12 +176,6 @@ impl Cmd { } parse_operations(&operations).ok_or(Error::MissingOperationResult) } - - pub fn run_in_sandbox(&self) -> Result { - // TODO: Implement this. This means we need to store ledger entries somewhere, and handle - // eviction, and restoration with that evicted state store. - todo!("Restoring ledger entries is not supported in the local sandbox mode"); - } } fn parse_operations(ops: &[OperationMeta]) -> Option { diff --git a/cmd/soroban-cli/src/commands/events.rs b/cmd/soroban-cli/src/commands/events.rs index 83809d42..002920de 100644 --- a/cmd/soroban-cli/src/commands/events.rs +++ b/cmd/soroban-cli/src/commands/events.rs @@ -3,18 +3,16 @@ use std::io; use soroban_env_host::xdr::{self, ReadXdr}; -use super::config::{events_file, locator, network}; -use crate::{rpc, toid, utils}; +use super::config::{locator, network}; +use crate::{rpc, utils}; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// The first ledger sequence number in the range to pull events (required - /// if not in sandbox mode). + /// 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. #[arg( long, @@ -22,17 +20,12 @@ pub struct Cmd { required_unless_present = "start_ledger" )] cursor: Option, - /// Output formatting options for event stream #[arg(long, value_enum, default_value = "pretty")] output: OutputFormat, - - /// The maximum number of events to display (specify "0" to show all events - /// when using sandbox, or to defer to the server-defined limit if using - /// RPC). + /// The maximum number of events to display (defer to the server-defined limit). #[arg(short, long, default_value = "10")] count: usize, - /// A set of (up to 5) contract IDs to filter events on. This parameter can /// be passed multiple times, e.g. `--id C123.. --id C456..`, or passed with /// multiple parameters, e.g. `--id C123 C456`. @@ -46,7 +39,6 @@ pub struct Cmd { help_heading = "FILTERS" )] contract_ids: Vec, - /// A set of (up to 4) topic filters to filter event topics on. A single /// topic filter can contain 1-4 different segment filters, separated by /// commas, with an asterisk (* character) indicating a wildcard segment. @@ -67,7 +59,6 @@ pub struct Cmd { help_heading = "FILTERS" )] topic_filters: Vec, - /// Specifies which type of contract events to display. #[arg( long = "type", @@ -76,79 +67,56 @@ pub struct Cmd { help_heading = "FILTERS" )] event_type: rpc::EventType, - #[command(flatten)] locator: locator::Args, - #[command(flatten)] network: network::Args, - - #[command(flatten)] - events_file: events_file::Args, } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("cursor is not valid")] InvalidCursor, - #[error("filepath does not exist: {path}")] InvalidFile { path: String }, - #[error("filepath ({path}) cannot be read: {error}")] CannotReadFile { path: String, error: String }, - #[error("cannot parse topic filter {topic} into 1-4 segments")] InvalidTopicFilter { topic: String }, - #[error("invalid segment ({segment}) in topic filter ({topic}): {error}")] InvalidSegment { topic: String, segment: String, error: xdr::Error, }, - #[error("cannot parse contract ID {contract_id}: {error}")] InvalidContractId { contract_id: String, error: stellar_strkey::DecodeError, }, - #[error("invalid JSON string: {error} ({debug})")] InvalidJson { debug: String, error: serde_json::Error, }, - #[error("invalid timestamp in event: {ts}")] InvalidTimestamp { ts: String }, - #[error("missing start_ledger and cursor")] MissingStartLedgerAndCursor, #[error("missing target")] MissingTarget, - #[error(transparent)] Rpc(#[from] rpc::Error), - #[error(transparent)] Generic(#[from] Box), - #[error(transparent)] Io(#[from] io::Error), - #[error(transparent)] Xdr(#[from] xdr::Error), - #[error(transparent)] Serde(#[from] serde_json::Error), - #[error(transparent)] Network(#[from] network::Error), - - #[error(transparent)] - EventsFile(#[from] events_file::Error), - #[error(transparent)] Locator(#[from] locator::Error), } @@ -194,11 +162,7 @@ impl Cmd { })?; } - let response = if self.network.is_no_network() { - self.run_in_sandbox() - } else { - self.run_against_rpc_server().await - }?; + let response = self.run_against_rpc_server().await?; for event in &response.events { match self.output { @@ -245,36 +209,6 @@ impl Cmd { .map_err(Error::Rpc) } - pub fn run_in_sandbox(&self) -> Result { - let start = self.start()?; - let count: usize = if self.count == 0 { - std::usize::MAX - } else { - self.count - }; - - let start_cursor = match start { - rpc::EventStart::Ledger(l) => (toid::Toid::new(l, 0, 0).into(), -1), - rpc::EventStart::Cursor(c) => rpc::parse_cursor(&c)?, - }; - let path = self.locator.config_dir()?; - let file = self.events_file.read(&path)?; - - // Read the JSON events from disk and find the ones that match the - // contract ID filter(s) that were passed in. - Ok(rpc::GetEventsResponse { - events: events_file::Args::filter_events( - &file.events, - &path, - start_cursor, - &self.contract_ids, - &self.topic_filters, - count, - ), - latest_ledger: file.latest_ledger, - }) - } - fn start(&self) -> Result { let start = match (self.start_ledger, self.cursor.clone()) { (Some(start), _) => rpc::EventStart::Ledger(start), @@ -285,95 +219,3 @@ impl Cmd { Ok(start) } } - -#[cfg(test)] -mod tests { - use std::path; - - use assert_fs::NamedTempFile; - use soroban_env_host::events; - use soroban_sdk::xdr::VecM; - - use super::*; - - use events_file::Args; - #[test] - fn test_does_event_serialization_match() { - let temp = NamedTempFile::new("events.json").unwrap(); - let events_file = Args { - events_file: Some(temp.to_path_buf()), - }; - // Make a couple of fake events with slightly different properties and - // write them to disk, then read the serialized versions from disk and - // ensure the properties match. - - let events: Vec = vec![ - events::HostEvent { - event: xdr::ContractEvent { - ext: xdr::ExtensionPoint::V0, - contract_id: Some(xdr::Hash([0; 32])), - type_: xdr::ContractEventType::Contract, - body: xdr::ContractEventBody::V0(xdr::ContractEventV0 { - topics: VecM::default(), - data: xdr::ScVal::U32(12345), - }), - }, - failed_call: false, - }, - events::HostEvent { - event: xdr::ContractEvent { - ext: xdr::ExtensionPoint::V0, - contract_id: Some(xdr::Hash([0x1; 32])), - type_: xdr::ContractEventType::Contract, - body: xdr::ContractEventBody::V0(xdr::ContractEventV0 { - topics: VecM::default(), - data: xdr::ScVal::I32(67890), - }), - }, - failed_call: false, - }, - ]; - - let ledger_info = soroban_ledger_snapshot::LedgerSnapshot { - protocol_version: 1, - sequence_number: 2, // this is the only value that matters - timestamp: 3, - network_id: [0x1; 32], - base_reserve: 5, - ledger_entries: vec![], - max_entry_expiration: 6, - min_persistent_entry_expiration: 7, - min_temp_entry_expiration: 8, - }; - - events_file.commit(&events, &ledger_info, &temp).unwrap(); - - let file = events_file.read(&std::env::current_dir().unwrap()).unwrap(); - assert_eq!(file.events.len(), 2); - assert_eq!(file.events[0].ledger, "2"); - assert_eq!(file.events[1].ledger, "2"); - assert_eq!( - file.events[0].contract_id, - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4" - ); - assert_eq!( - file.events[1].contract_id, - "CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526" - ); - assert_eq!(file.latest_ledger, 2); - } - - #[test] - fn test_does_event_fixture_load() { - // This test ensures that the included JSON fixture file matches the - // correct event format (for the purposes of human readability). - let filename = - path::PathBuf::from("../crates/soroban-test/tests/fixtures/test-jsons/get-events.json"); - let events_file = Args { - events_file: Some(filename), - }; - let result = events_file.read(&std::env::current_dir().unwrap()); - println!("{result:?}"); - assert!(result.is_ok()); - } -} diff --git a/cmd/soroban-cli/src/commands/lab/token/wrap.rs b/cmd/soroban-cli/src/commands/lab/token/wrap.rs index 4cd2dfb0..b2f878ca 100644 --- a/cmd/soroban-cli/src/commands/lab/token/wrap.rs +++ b/cmd/soroban-cli/src/commands/lab/token/wrap.rs @@ -1,17 +1,15 @@ use clap::{arg, command, Parser}; use soroban_env_host::{ - budget::Budget, - storage::Storage, xdr::{ Asset, ContractDataDurability, ContractExecutable, ContractIdPreimage, CreateContractArgs, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, LedgerKey::ContractData, LedgerKeyContractData, Memo, MuxedAccount, Operation, OperationBody, Preconditions, ScAddress, ScVal, SequenceNumber, Transaction, TransactionExt, Uint256, VecM, }, - Host, HostError, + HostError, }; use std::convert::Infallible; -use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError, rc::Rc}; +use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ commands::config, @@ -63,42 +61,11 @@ impl Cmd { // Parse asset let asset = parse_asset(&self.asset)?; - let res_str = if self.config.is_no_network() { - self.run_in_sandbox(&asset)? - } else { - self.run_against_rpc_server(asset).await? - }; + let res_str = self.run_against_rpc_server(asset).await?; println!("{res_str}"); Ok(()) } - pub fn run_in_sandbox(&self, asset: &Asset) -> Result { - // Initialize storage and host - // TODO: allow option to separate input and output file - let mut state = self.config.get_state()?; - - let snap = Rc::new(state.clone()); - let h = Host::with_storage_and_budget( - Storage::with_recording_footprint(snap), - Budget::default(), - ); - - let mut ledger_info = state.ledger_info(); - ledger_info.sequence_number += 1; - ledger_info.timestamp += 5; - h.set_ledger_info(ledger_info)?; - - let res = h.invoke_function(HostFunction::CreateContract(CreateContractArgs { - contract_id_preimage: ContractIdPreimage::Asset(asset.clone()), - executable: ContractExecutable::Token, - }))?; - - let contract_id = vec_to_hash(&res)?; - state.update(&h); - self.config.set_state(&state)?; - Ok(stellar_strkey::Contract(contract_id.0).to_string()) - } - async fn run_against_rpc_server(&self, asset: Asset) -> Result { let network = self.config.get_network()?; let client = Client::new(&network.rpc_url)?; diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 295144f9..a9f66808 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -11,7 +11,6 @@ pub mod lab; pub mod plugin; pub mod version; -pub const HEADING_SANDBOX: &str = "Options (Sandbox)"; pub const HEADING_RPC: &str = "Options (RPC)"; const ABOUT: &str = "Build, deploy, & interact with contracts; set identities to sign with; configure networks; generate keys; and more. @@ -33,11 +32,13 @@ Commands that relate to smart contract interactions are organized under the `con 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. `soroban contract invoke` makes use of this: - soroban contract invoke --id 1 --source alice -- --help + soroban 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: - soroban contract invoke --id 1 --source alice -- hello --to world + soroban contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- \ + hello --to world Full CLI reference: https://github.com/stellar/soroban-tools/tree/main/docs/soroban-cli-full-docs.md"; diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 1589bc48..3aad487c 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -7,7 +7,6 @@ pub mod commands; pub mod fee; pub mod key; pub mod log; -pub mod network; pub mod rpc; pub mod toid; pub mod utils; diff --git a/cmd/soroban-cli/src/network.rs b/cmd/soroban-cli/src/network.rs deleted file mode 100644 index d0bc3427..00000000 --- a/cmd/soroban-cli/src/network.rs +++ /dev/null @@ -1,8 +0,0 @@ -use sha2::{Digest, Sha256}; - -pub static SANDBOX_NETWORK_PASSPHRASE: &str = "Local Sandbox Stellar Network ; September 2022"; - -#[must_use] -pub fn sandbox_network_id() -> [u8; 32] { - Sha256::digest(SANDBOX_NETWORK_PASSPHRASE.as_bytes()).into() -} diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 0cbf9dfb..fcc3964c 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -1,28 +1,13 @@ -use std::hash::BuildHasher; -use std::{collections::HashMap, io::ErrorKind, path::Path}; - use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; - -use soroban_env_host::{ - storage::{AccessType, Footprint}, - xdr::{ - AccountEntry, AccountEntryExt, AccountId, Asset, ContractCodeEntry, ContractDataDurability, - ContractDataEntry, ContractExecutable, ContractIdPreimage, DecoratedSignature, - Error as XdrError, ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageContractId, - LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerFootprint, LedgerKey, - LedgerKeyContractCode, LedgerKeyContractData, ScAddress, ScContractInstance, ScSpecEntry, - ScVal, SequenceNumber, Signature, SignatureHint, String32, Thresholds, Transaction, - TransactionEnvelope, TransactionSignaturePayload, - TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, VecM, WriteXdr, - }, -}; -use soroban_ledger_snapshot::LedgerSnapshot; -use soroban_sdk::token; -use soroban_spec::read::FromWasmError; use stellar_strkey::ed25519::PrivateKey; -use crate::network::sandbox_network_id; +use soroban_env_host::xdr::{ + Asset, ContractIdPreimage, DecoratedSignature, Error as XdrError, Hash, HashIdPreimage, + HashIdPreimageContractId, Signature, SignatureHint, Transaction, TransactionEnvelope, + TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, + TransactionV1Envelope, WriteXdr, +}; pub mod contract_spec; @@ -33,122 +18,6 @@ pub fn contract_hash(contract: &[u8]) -> Result { Ok(Hash(Sha256::digest(contract).into())) } -/// # Errors -/// -/// Might return an error -pub fn ledger_snapshot_read_or_default( - p: impl AsRef, -) -> Result { - match LedgerSnapshot::read_file(p) { - Ok(snapshot) => Ok(snapshot), - Err(soroban_ledger_snapshot::Error::Io(e)) if e.kind() == ErrorKind::NotFound => { - Ok(LedgerSnapshot { - network_id: sandbox_network_id(), - // These three "defaults" are not part of the actual default definition in - // rs-soroban-sdk, but if we don't have them the sandbox doesn't work right. - // Oof. - // TODO: Remove this hacky workaround. - min_persistent_entry_expiration: 4096, - min_temp_entry_expiration: 16, - max_entry_expiration: 6_312_000, - ..Default::default() - }) - } - Err(e) => Err(e), - } -} - -type LedgerSnapshotEntries = Vec<(Box, (Box, Option))>; - -/// # Errors -/// -/// Might return an error -pub fn add_contract_code_to_ledger_entries( - entries: &mut LedgerSnapshotEntries, - contract: Vec, - min_persistent_entry_expiration: u32, -) -> Result { - // Install the code - let hash = contract_hash(contract.as_slice())?; - let code_key = LedgerKey::ContractCode(LedgerKeyContractCode { hash: hash.clone() }); - let code_entry = LedgerEntry { - last_modified_ledger_seq: 0, - data: LedgerEntryData::ContractCode(ContractCodeEntry { - ext: ExtensionPoint::V0, - hash: hash.clone(), - code: contract.try_into()?, - }), - ext: LedgerEntryExt::V0, - }; - for (k, e) in &mut *entries { - if **k == code_key { - *e = (Box::new(code_entry), Some(min_persistent_entry_expiration)); - return Ok(hash); - } - } - entries.push(( - Box::new(code_key), - (Box::new(code_entry), Some(min_persistent_entry_expiration)), - )); - Ok(hash) -} - -pub fn add_contract_to_ledger_entries( - entries: &mut LedgerSnapshotEntries, - contract_id: [u8; 32], - wasm_hash: [u8; 32], - min_persistent_entry_expiration: u32, -) { - // Create the contract - let contract_key = LedgerKey::ContractData(LedgerKeyContractData { - contract: ScAddress::Contract(contract_id.into()), - key: ScVal::LedgerKeyContractInstance, - durability: ContractDataDurability::Persistent, - }); - - let contract_entry = LedgerEntry { - last_modified_ledger_seq: 0, - data: LedgerEntryData::ContractData(ContractDataEntry { - contract: ScAddress::Contract(contract_id.into()), - key: ScVal::LedgerKeyContractInstance, - durability: ContractDataDurability::Persistent, - val: ScVal::ContractInstance(ScContractInstance { - executable: ContractExecutable::Wasm(Hash(wasm_hash)), - storage: None, - }), - ext: ExtensionPoint::V0, - }), - ext: LedgerEntryExt::V0, - }; - for (k, e) in &mut *entries { - if **k == contract_key { - *e = ( - Box::new(contract_entry), - Some(min_persistent_entry_expiration), - ); - return; - } - } - entries.push(( - Box::new(contract_key), - ( - Box::new(contract_entry), - Some(min_persistent_entry_expiration), - ), - )); -} - -pub fn bump_ledger_entry_expirations( - entries: &mut LedgerSnapshotEntries, - lookup: &HashMap, -) { - for (k, (_, expiration)) in &mut *entries { - if let Some(min_expiration) = lookup.get(k.as_ref()) { - *expiration = Some(*min_expiration); - } - } -} - /// # Errors /// /// Might return an error @@ -198,129 +67,6 @@ pub fn contract_id_from_str(contract_id: &str) -> Result<[u8; 32], stellar_strke .map_err(|_| stellar_strkey::DecodeError::Invalid) } -fn get_entry_from_snapshot( - key: &LedgerKey, - entries: &LedgerSnapshotEntries, -) -> Option<(Box, Option)> { - for (k, result) in entries { - if *key == **k { - return Some((*result).clone()); - } - } - None -} - -/// # Errors -/// -/// Might return an error -pub fn get_contract_spec_from_state( - state: &LedgerSnapshot, - contract_id: [u8; 32], -) -> Result, FromWasmError> { - let current_ledger_seq = state.sequence_number; - let key = LedgerKey::ContractData(LedgerKeyContractData { - contract: ScAddress::Contract(contract_id.into()), - key: ScVal::LedgerKeyContractInstance, - durability: ContractDataDurability::Persistent, - }); - let (entry, expiration_ledger_seq) = match get_entry_from_snapshot(&key, &state.ledger_entries) - { - // It's a contract data entry, so it should have an expiration if present - Some((entry, expiration)) => (entry, expiration.unwrap()), - None => return Err(FromWasmError::NotFound), - }; - if expiration_ledger_seq <= current_ledger_seq { - return Err(FromWasmError::NotFound); - } - - match *entry { - LedgerEntry { - data: - LedgerEntryData::ContractData(ContractDataEntry { - val: ScVal::ContractInstance(ScContractInstance { executable, .. }), - .. - }), - .. - } => match executable { - ContractExecutable::Token => { - // TODO/FIXME: I don't think it will work for token contracts, since we don't store them in the state? - let res = soroban_spec::read::parse_raw(&token::StellarAssetSpec::spec_xdr()); - res.map_err(FromWasmError::Parse) - } - ContractExecutable::Wasm(hash) => { - // It's a contract code entry, so it should have an expiration if present - let (entry, expiration_ledger_seq) = match get_entry_from_snapshot( - &LedgerKey::ContractCode(LedgerKeyContractCode { hash: hash.clone() }), - &state.ledger_entries, - ) { - // It's a contract data entry, so it should have an expiration if present - Some((entry, expiration)) => (entry, expiration.unwrap()), - None => return Err(FromWasmError::NotFound), - }; - if expiration_ledger_seq <= current_ledger_seq { - return Err(FromWasmError::NotFound); - } - match *entry { - LedgerEntry { - data: LedgerEntryData::ContractCode(ContractCodeEntry { code, .. }), - .. - } => soroban_spec::read::from_wasm(code.as_vec()), - _ => Err(FromWasmError::NotFound), - } - } - }, - _ => Err(FromWasmError::NotFound), - } -} - -/// # Panics -/// -/// May panic -#[must_use] -pub fn create_ledger_footprint(footprint: &Footprint) -> LedgerFootprint { - let mut read_only: Vec = vec![]; - let mut read_write: Vec = vec![]; - let Footprint(m) = footprint; - for (k, v) in m { - let dest = match v { - AccessType::ReadOnly => &mut read_only, - AccessType::ReadWrite => &mut read_write, - }; - dest.push((**k).clone()); - } - LedgerFootprint { - read_only: read_only.try_into().unwrap(), - read_write: read_write.try_into().unwrap(), - } -} - -#[must_use] -pub fn default_account_ledger_entry(account_id: AccountId) -> LedgerEntry { - // TODO: Consider moving the definition of a default account ledger entry to - // a location shared by the SDK and CLI. The SDK currently defines the same - // value (see URL below). There's some benefit in only defining this once to - // prevent the two from diverging, which would cause inconsistent test - // behavior between the SDK and CLI. A good home for this is unclear at this - // time. - // https://github.com/stellar/rs-soroban-sdk/blob/b6f9a2c7ec54d2d5b5a1e02d1e38ae3158c22e78/soroban-sdk/src/accounts.rs#L470-L483. - LedgerEntry { - data: LedgerEntryData::Account(AccountEntry { - account_id, - balance: 0, - flags: 0, - home_domain: String32::default(), - inflation_dest: None, - num_sub_entries: 0, - seq_num: SequenceNumber(0), - thresholds: Thresholds([1; 4]), - signers: VecM::default(), - ext: AccountEntryExt::V0, - }), - last_modified_ledger_seq: 0, - ext: LedgerEntryExt::V0, - } -} - /// # Errors /// May not find a config dir pub fn find_config_dir(mut pwd: std::path::PathBuf) -> std::io::Result { diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index 1634df6c..ba39b772 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -25,7 +25,7 @@ import ( func cargoTest(t *testing.T, name string) { NewCLITest(t) - c := icmd.Command("cargo", "test", "--package", "soroban-test", "--test", "it", "--", name, "--exact", "--nocapture") + c := icmd.Command("cargo", "test", "--features", "integration", "--package", "soroban-test", "--test", "it", "--", name, "--exact", "--nocapture") c.Env = append(os.Environ(), fmt.Sprintf("SOROBAN_RPC_URL=http://localhost:%d/", sorobanRPCPort), fmt.Sprintf("SOROBAN_NETWORK_PASSPHRASE=%s", StandaloneNetworkPassphrase), @@ -35,9 +35,9 @@ func cargoTest(t *testing.T, name string) { } func TestCLICargoTest(t *testing.T) { - names := icmd.RunCmd(icmd.Command("cargo", "-q", "test", "integration_and_sandbox::", "--package", "soroban-test", "--", "--list")) + names := icmd.RunCmd(icmd.Command("cargo", "-q", "test", "integration::", "--package", "soroban-test", "--features", "integration", "--", "--list")) input := names.Stdout() - lines := strings.Split(input, "\n") + lines := strings.Split(strings.TrimSpace(input), "\n") for _, line := range lines { testName := strings.TrimSuffix(line, ": test") t.Run(testName, func(t *testing.T) { diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index 35bb7697..e4768247 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -62,11 +62,11 @@ Commands that relate to smart contract interactions are organized under the `con 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. `soroban contract invoke` makes use of this: - soroban contract invoke --id 1 --source alice -- --help + soroban 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: - soroban contract invoke --id 1 --source alice -- hello --to world + soroban contract invoke --id CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2 --source alice --network testnet -- hello --to world Full CLI reference: https://github.com/stellar/soroban-tools/tree/main/docs/soroban-cli-full-docs.md @@ -129,7 +129,7 @@ Tools for smart contract developers * `build` — Build a contract from source * `bump` — Extend the expiry ledger of a contract-data ledger entry * `deploy` — Deploy a contract -* `fetch` — Fetch a contract's Wasm binary from a network or local sandbox +* `fetch` — Fetch a contract's Wasm binary * `inspect` — Inspect a WASM file listing contract functions, meta, etc * `install` — Install a WASM file to the ledger without creating a contract instance * `invoke` — Invoke a contract function @@ -252,7 +252,6 @@ If no keys are specified the contract itself is bumped. * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -273,12 +272,10 @@ Deploy a contract * `--wasm ` — WASM file to deploy * `--wasm-hash ` — Hash of the already installed/deployed WASM file -* `--id ` — Contract ID to deploy to * `--salt ` — Custom salt 32-byte salt for the token id * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -291,7 +288,7 @@ Deploy a contract ## `soroban contract fetch` -Fetch a contract's Wasm binary from a network or local sandbox +Fetch a contract's Wasm binary **Usage:** `soroban contract fetch [OPTIONS] --id ` @@ -304,7 +301,6 @@ Fetch a contract's Wasm binary from a network or local sandbox * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` @@ -345,7 +341,6 @@ Install a WASM file to the ledger without creating a contract instance * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -374,18 +369,14 @@ soroban contract invoke ... -- --help ###### **Options:** * `--id ` — Contract ID to invoke -* `--wasm ` — WASM file of the contract to invoke (if using sandbox will deploy this file) * `--cost` — Output the cost execution to stderr -* `--unlimited-budget` — Run with an unlimited budget * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config * `--config-dir ` -* `--events-file ` — File to persist events, default is `.soroban/events.json` * `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm Default value: `100` @@ -441,7 +432,6 @@ Print the current value of a contract-data ledger entry * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -476,7 +466,6 @@ If no keys are specificed the contract itself is restored. * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -720,7 +709,7 @@ Watch the network for contract events ###### **Options:** -* `--start-ledger ` — The first ledger sequence number in the range to pull events (required if not in sandbox mode). 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 @@ -734,7 +723,7 @@ Watch the network for contract events - `json`: JSONified console output -* `-c`, `--count ` — The maximum number of events to display (specify "0" to show all events when using sandbox, or to defer to the server-defined limit if using RPC) +* `-c`, `--count ` — The maximum number of events to display (defer to the server-defined limit) Default value: `10` * `--id ` — A set of (up to 5) contract IDs to filter events on. This parameter can be passed multiple times, e.g. `--id C123.. --id C456..`, or passed with multiple parameters, e.g. `--id C123 C456` @@ -750,7 +739,6 @@ Watch the network for contract events * `--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 -* `--events-file ` — File to persist events, default is `.soroban/events.json` @@ -792,7 +780,6 @@ Deploy a token contract to wrap an existing Stellar classic asset for smart cont * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config @@ -815,7 +802,6 @@ Compute the expected contract id for the given asset * `--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 -* `--ledger-file ` — File to persist ledger state, default is `.soroban/ledger.json` * `--source-account ` — Account that signs the final transaction. Alias `source`. Can be an identity (--source alice), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). Default: `identity generate --default-seed` * `--hd-path ` — If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` * `--global` — Use global config