diff --git a/.gitignore b/.gitignore index 7af275d48..00ec6c1d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .soroban/ +!test.toml diff --git a/Cargo.lock b/Cargo.lock index 3f9b41bf1..69d7d16ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -316,6 +327,25 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "config" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f1667b8320afa80d69d8bbe40830df2c8a06003d86f73d8e003b2c48df416d" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -362,6 +392,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "csv" version = "1.1.6" @@ -386,9 +426,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -499,8 +539,35 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", "crypto-common", + "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "doc-comment" version = "0.3.3" @@ -700,6 +767,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "headers" @@ -759,6 +829,25 @@ dependencies = [ "serde", ] +[[package]] +name = "hmac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "http" version = "0.2.8" @@ -955,6 +1044,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonrpsee-core" version = "0.15.1" @@ -1035,6 +1135,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "lock_api" version = "0.4.9" @@ -1082,6 +1188,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.6.2" @@ -1121,6 +1233,16 @@ dependencies = [ "twoway", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -1211,6 +1333,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -1240,12 +1372,71 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" +dependencies = [ + "once_cell", + "pest", + "sha1", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -1457,6 +1648,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.8", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.7.1" @@ -1504,6 +1706,48 @@ dependencies = [ "winapi", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags", + "serde", +] + +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -1645,6 +1889,18 @@ dependencies = [ "libc", ] +[[package]] +name = "sep5" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afe34ccbd1fb6fa0b2fc7cccb037bd3d3f1e484c3befe1b713d7611884f336a" +dependencies = [ + "slip10", + "stellar-strkey 0.0.7", + "thiserror", + "tiny-bip39", +] + [[package]] name = "serde" version = "1.0.152" @@ -1786,6 +2042,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slip10" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28724a6e6f70b0cb115c580891483da6f3aa99e6a353598303a57f89d23aa6bc" +dependencies = [ + "ed25519-dalek", + "hmac 0.9.0", + "sha2 0.9.9", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -1820,8 +2087,10 @@ dependencies = [ "chrono", "clap", "clap_complete", + "config", "crate-git-revision", "csv", + "dirs", "ed25519-dalek", "hex", "jsonrpsee-core", @@ -1830,6 +2099,8 @@ dependencies = [ "once_cell", "rand 0.8.5", "regex", + "rpassword", + "sep5", "serde", "serde_derive", "serde_json", @@ -1844,6 +2115,7 @@ dependencies = [ "termcolor_output", "thiserror", "tokio", + "toml", "warp", "wasm-opt", "wasmparser 0.90.0", @@ -2043,7 +2315,8 @@ dependencies = [ [[package]] name = "stellar-strkey" version = "0.0.7" -source = "git+https://github.com/stellar/rs-stellar-strkey?rev=6e9bb10#6e9bb102249585e851bf36be2ab069a1955c3b09" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0689070126ca7f2effc2c5726584446db52190f0cef043c02eb4040a711c11" dependencies = [ "base32", "thiserror", @@ -2254,6 +2527,25 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac 0.12.1", + "once_cell", + "pbkdf2", + "rand 0.8.5", + "rustc-hash", + "sha2 0.10.6", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2348,6 +2640,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2437,6 +2738,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "unicase" version = "2.6.0" @@ -2826,11 +3133,20 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 3275097bb..502b7aa26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,10 @@ rev = "e1c3de33942f0e997680645941787102ebf61e85" [workspace.dependencies.stellar-strkey] version = "0.0.7" -git = "https://github.com/stellar/rs-stellar-strkey" -rev = "6e9bb10" + +[workspace.dependencies.sep5] +version = "0.0.2" + # [patch."https://github.com/stellar/rs-soroban-env"] # soroban-env-host = { path = "../rs-soroban-env/soroban-env-host/" } diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 68b9cdde8..61dc61dfa 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -46,6 +46,12 @@ regex = "1.6.0" wasm-opt = "0.111.0" chrono = "0.4.23" once_cell = "1.16.0" +rpassword = "7.2.0" +dirs = "4.0.0" +config = "0.13.1" +toml = "0.5.9" +sep5 = { workspace = true} + [build-dependencies] crate-git-revision = "0.0.4" diff --git a/cmd/soroban-cli/src/config/identity/add.rs b/cmd/soroban-cli/src/config/identity/add.rs new file mode 100644 index 000000000..27f0a31b8 --- /dev/null +++ b/cmd/soroban-cli/src/config/identity/add.rs @@ -0,0 +1,34 @@ +use crate::config::{locator, secret}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Secret(#[from] secret::Error), + + #[error(transparent)] + Config(#[from] locator::Error), +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + /// Name of identity + pub name: String, + + #[clap(flatten)] + pub secrets: secret::Args, + + /// Set as default identity + #[clap(long)] + pub default: bool, + + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + Ok(self + .config_locator + .write_identity(&self.name, &self.secrets.read_secret()?)?) + } +} diff --git a/cmd/soroban-cli/src/config/identity/generate.rs b/cmd/soroban-cli/src/config/identity/generate.rs new file mode 100644 index 000000000..2cd0883ac --- /dev/null +++ b/cmd/soroban-cli/src/config/identity/generate.rs @@ -0,0 +1,33 @@ +use crate::config::{ + locator, + secret::{self, Secret}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), + #[error(transparent)] + Secret(#[from] secret::Error), +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + /// Name of identity + pub name: String, + /// Optional seed to use when generating seed phrase. + /// Random otherwise. + #[clap(long, short = 's')] + pub seed: Option, + + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let secret = Secret::from_seed(self.seed.as_ref())?; + self.config_locator.write_identity(&self.name, &secret)?; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/config/identity/ls.rs b/cmd/soroban-cli/src/config/identity/ls.rs new file mode 100644 index 000000000..e479dac80 --- /dev/null +++ b/cmd/soroban-cli/src/config/identity/ls.rs @@ -0,0 +1,21 @@ +use crate::config::locator; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let res = self.config_locator.list_identities()?; + println!("{}", res.join("\n")); + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/config/identity/mod.rs b/cmd/soroban-cli/src/config/identity/mod.rs new file mode 100644 index 000000000..d561bddc9 --- /dev/null +++ b/cmd/soroban-cli/src/config/identity/mod.rs @@ -0,0 +1,45 @@ +use clap::Parser; + +pub mod add; +pub mod generate; +pub mod ls; +pub mod rm; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// Add a new identity (keypair, ledger, macOS keychain) + Add(add::Cmd), + /// Generate a new identity with a seed phrase, currently 12 words + Generate(generate::Cmd), + /// List identities + Ls(ls::Cmd), + /// Remove an identity + Rm(rm::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Add(#[from] add::Error), + + #[error(transparent)] + Generate(#[from] generate::Error), + + #[error(transparent)] + Rm(#[from] rm::Error), + + #[error(transparent)] + Ls(#[from] ls::Error), +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + match self { + Cmd::Add(cmd) => cmd.run()?, + Cmd::Rm(new) => new.run()?, + Cmd::Ls(cmd) => cmd.run()?, + Cmd::Generate(cmd) => cmd.run()?, + }; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/config/identity/rm.rs b/cmd/soroban-cli/src/config/identity/rm.rs new file mode 100644 index 000000000..a9124d68d --- /dev/null +++ b/cmd/soroban-cli/src/config/identity/rm.rs @@ -0,0 +1,32 @@ +use crate::config::locator; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("No such identity {name}")] + MissingIdentity { name: String }, + #[error("Error deleting {path}")] + DeletingIdFile { path: String }, +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + /// default name + pub default_name: String, + + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let path = self + .config_locator + .identity_path(&self.default_name) + .map_err(|_| Error::MissingIdentity { + name: self.default_name.clone(), + })?; + std::fs::remove_file(&path).map_err(|_| Error::DeletingIdFile { + path: format!("{}", path.display()), + }) + } +} diff --git a/cmd/soroban-cli/src/config/ledger_file.rs b/cmd/soroban-cli/src/config/ledger_file.rs new file mode 100644 index 000000000..c5384f0f2 --- /dev/null +++ b/cmd/soroban-cli/src/config/ledger_file.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use soroban_ledger_snapshot::LedgerSnapshot; + +use crate::{utils, HEADING_SANDBOX}; + +#[derive(Debug, clap::Args, Clone)] +pub struct Args { + /// File to persist ledger state + #[clap( + long, + parse(from_os_str), + default_value(".soroban/ledger.json"), + conflicts_with = "rpc-url", + env = "SOROBAN_LEDGER_FILE", + help_heading = HEADING_SANDBOX, + )] + pub ledger_file: PathBuf, +} + +#[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) -> Result { + utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { + Error::CannotReadLedgerFile { + filepath: self.ledger_file.clone(), + error: e, + } + }) + } + + pub fn write(&self, state: &mut LedgerSnapshot) -> Result<(), Error> { + state + .write_file(&self.ledger_file) + .map_err(|e| Error::CannotCommitLedgerFile { + filepath: self.ledger_file.clone(), + error: e, + }) + } +} diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs new file mode 100644 index 000000000..e0f50dc82 --- /dev/null +++ b/cmd/soroban-cli/src/config/locator.rs @@ -0,0 +1,164 @@ +use std::{ + ffi::OsStr, + fs, io, + path::{Path, PathBuf}, + str::FromStr, +}; + +use crate::utils::find_config_dir; + +use super::{network::Network, secret::Secret}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Failed to find home directory")] + HomeDirNotFound, + #[error("Failed read current directory")] + CurrentDirNotFound, + #[error("Failed to create directory: {path:?}")] + DirCreationFailed { path: PathBuf }, + #[error("Failed to read secret's file: {path}")] + SecretFileRead { path: PathBuf }, + #[error("Failed to read network file: {path}")] + NetworkFileRead { path: PathBuf }, + #[error("Seceret file failed to deserialize")] + Deserialization, + #[error("Failed to write identity file:{filepath}: {error}")] + IdCreationFailed { filepath: PathBuf, error: io::Error }, + #[error("Seceret file failed to deserialize")] + NetworkDeserialization, + #[error("Failed to write network file")] + NetworkCreationFailed, + #[error("Error Identity directory is invalid: {name}")] + IdentityList { name: String }, + // #[error("Config file failed to deserialize")] + // CannotReadConfigFile, + #[error("Config file failed to serialize")] + ConfigSerialization, + // #[error("Config file failed write")] + // CannotWriteConfigFile, + #[error("XDG_CONFIG_HOME env variable is not a valid path. Got {value}")] + XdgConfigHome { value: String }, +} + +#[derive(Debug, clap::Args, Default, Clone)] +pub struct Args { + /// Use global config + #[clap(long)] + pub global: bool, +} + +impl Args { + pub fn config_dir(&self) -> Result { + let config_dir = if self.global { + if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") { + PathBuf::from_str(&config_home) + .map_err(|_| Error::XdgConfigHome { value: config_home })? + } else { + dirs::home_dir() + .ok_or(Error::HomeDirNotFound)? + .join(".config") + } + .join("soroban") + } else { + let pwd = std::env::current_dir().map_err(|_| Error::CurrentDirNotFound)?; + find_config_dir(pwd.clone()).unwrap_or_else(|_| pwd.join(".soroban")) + }; + ensure_directory(config_dir) + } + + pub fn identity_dir(&self) -> Result { + ensure_directory(self.config_dir()?.join("identities")) + } + + pub fn network_dir(&self) -> Result { + ensure_directory(self.config_dir()?.join("networks")) + } + + pub fn identity_path(&self, name: &str) -> Result { + self.identity_dir().map(|p| { + let mut source = p.join(name); + source.set_extension("toml"); + source + }) + } + + pub fn network_path(&self, name: &str) -> Result { + self.network_dir().map(|p| { + let mut source = p.join(name); + source.set_extension("toml"); + source + }) + } + + pub fn write_identity(&self, name: &str, secret: &Secret) -> Result<(), Error> { + let source = self.identity_path(name)?; + let data = toml::to_string(secret).map_err(|_| Error::ConfigSerialization)?; + std::fs::write(&source, data).map_err(|error| Error::IdCreationFailed { + filepath: source.clone(), + error, + }) + } + + pub fn write_network(&self, name: &str, network: &Network) -> Result<(), Error> { + let source = self.network_path(name)?; + let data = toml::to_string(network).map_err(|_| Error::Deserialization)?; + std::fs::write(source, data).map_err(|_| Error::NetworkCreationFailed) + } + + pub fn list_identities(&self) -> Result, Error> { + let path = self.identity_dir()?; + read_dir(&path) + } + + pub fn list_networks(&self) -> Result, Error> { + let path = self.network_dir()?; + read_dir(&path) + } +} + +pub fn read_identity(name: &str) -> Result { + // 1. check workspace config files for `name` + let local_identity = Args { global: false }.identity_path(name); + // 2. use if found, else, check global config files for `name` + let path = local_identity.or_else(|_| Args { global: true }.identity_path(name))?; + let data = fs::read(&path).map_err(|_| Error::SecretFileRead { path })?; + toml::from_slice::(&data).map_err(|_| Error::Deserialization) +} + +pub fn read_network(name: &str) -> Result { + // 1. check workspace config files for `name` + let local_network = Args { global: false }.network_path(name); + // 2. use if found, else, check global config files for `name` + let path = local_network.or_else(|_| Args { global: true }.network_path(name))?; + let data = fs::read(&path).map_err(|_| Error::NetworkFileRead { path })?; + toml::from_slice::(&data).map_err(|_| Error::NetworkDeserialization) +} + +fn ensure_directory(dir: PathBuf) -> Result { + std::fs::create_dir_all(&dir).map_err(|_| dir_creation_failed(&dir))?; + Ok(dir) +} + +fn dir_creation_failed(p: &Path) -> Error { + Error::DirCreationFailed { + path: p.to_path_buf(), + } +} + +fn read_dir(dir: &Path) -> Result, Error> { + let contents = std::fs::read_dir(dir).map_err(|_| Error::IdentityList { + name: format!("{}", dir.display()), + })?; + let mut res = vec![]; + for entry in contents.filter_map(Result::ok) { + let path = entry.path(); + if let Some("toml") = path.extension().and_then(OsStr::to_str) { + if let Some(os_str) = path.file_stem() { + res.push(os_str.to_string_lossy().trim().to_string()); + } + } + } + res.sort(); + Ok(res) +} diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs new file mode 100644 index 000000000..85ca8ca97 --- /dev/null +++ b/cmd/soroban-cli/src/config/mod.rs @@ -0,0 +1,110 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; +use soroban_ledger_snapshot::LedgerSnapshot; + +use self::network::Network; + +pub mod identity; +pub mod ledger_file; +pub mod locator; +pub mod network; +pub mod secret; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// Configure different identities to sign transactions. + #[clap(subcommand)] + Identity(identity::Cmd), + + /// Configure different networks + #[clap(subcommand)] + Network(network::Cmd), +} + +#[derive(thiserror::Error, Debug)] +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), + + #[error("cannot parse secret key")] + CannotParseSecretKey, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + match &self { + Cmd::Identity(identity) => identity.run()?, + Cmd::Network(network) => network.run()?, + } + Ok(()) + } +} + +#[derive(Debug, clap::Args, Clone)] +pub struct Args { + /// Secret Key used to sign transaction sent to the rpc server + #[clap(long)] + pub secret_key: Option, + + #[clap(flatten)] + pub network: network::Args, + + #[clap(flatten)] + pub ledger_file: ledger_file::Args, + + #[clap(long, alias = "as")] + /// Use specified identity to sign transaction + pub identity: Option, + + #[clap(long)] + /// If using a seed phrase, which hd path to use, e.g. `m/44'/148'/{hd_path}` + pub hd_path: Option, +} + +impl Args { + pub fn key_pair(&self) -> Result { + let key = if let Some(identity) = &self.identity { + locator::read_identity(identity)? + } else if let Some(secret_key) = &self.secret_key { + secret::Secret::SecretKey { + secret_key: secret_key.clone(), + } + } else { + return Err(Error::CannotParseSecretKey); + }; + Ok(key.key_pair(self.hd_path)?) + } + + pub fn get_network(&self) -> Result { + Ok(self.network.get_network()?) + } + + pub fn is_no_network(&self) -> bool { + self.network.network.is_none() + && self.network.network_passphrase.is_none() + && self.network.rpc_url.is_none() + } + + pub fn get_state(&self) -> Result { + Ok(self.ledger_file.read()?) + } + + pub fn set_state(&self, state: &mut LedgerSnapshot) -> Result<(), Error> { + Ok(self.ledger_file.write(state)?) + } +} + +#[derive(Default, Serialize, Deserialize)] +pub struct Config {} diff --git a/cmd/soroban-cli/src/config/network/add.rs b/cmd/soroban-cli/src/config/network/add.rs new file mode 100644 index 000000000..7b7024ea8 --- /dev/null +++ b/cmd/soroban-cli/src/config/network/add.rs @@ -0,0 +1,37 @@ +use crate::config::{locator, secret}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Secret(#[from] secret::Error), + + #[error(transparent)] + Config(#[from] locator::Error), + + #[error("Failed to write network file")] + NetworkCreationFailed, +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + /// Name of network + pub name: String, + + #[clap(flatten)] + pub network: super::Network, + + /// Set as default network + #[clap(long)] + pub default: bool, + + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + self.config_locator + .write_network(&self.name, &self.network) + .map_err(|_| Error::NetworkCreationFailed) + } +} diff --git a/cmd/soroban-cli/src/config/network/ls.rs b/cmd/soroban-cli/src/config/network/ls.rs new file mode 100644 index 000000000..05017b247 --- /dev/null +++ b/cmd/soroban-cli/src/config/network/ls.rs @@ -0,0 +1,21 @@ +use crate::config::locator; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Config(#[from] locator::Error), +} + +#[derive(Debug, clap::Args, Clone)] +pub struct Cmd { + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let res = self.config_locator.list_networks()?; + println!("{}", res.join("\n")); + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/config/network/mod.rs b/cmd/soroban-cli/src/config/network/mod.rs new file mode 100644 index 000000000..d0aba0a5b --- /dev/null +++ b/cmd/soroban-cli/src/config/network/mod.rs @@ -0,0 +1,105 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; + +use crate::HEADING_RPC; + +use super::locator; + +pub mod add; +pub mod ls; +pub mod rm; + +#[derive(Debug, Parser)] +pub enum Cmd { + /// Add a new network + Add(add::Cmd), + /// Remove a network + Rm(rm::Cmd), + /// List networks + Ls(ls::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Add(#[from] add::Error), + + #[error(transparent)] + Rm(#[from] rm::Error), + + #[error(transparent)] + Ls(#[from] ls::Error), + + #[error(transparent)] + Config(#[from] locator::Error), +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + match self { + Cmd::Add(cmd) => cmd.run()?, + Cmd::Rm(new) => new.run()?, + Cmd::Ls(cmd) => cmd.run()?, + }; + Ok(()) + } +} + +#[derive(Debug, clap::Args, Clone)] +pub struct Args { + /// RPC server endpoint + #[clap( + long, + requires = "secret-key", + requires = "network-passphrase", + env = "SOROBAN_RPC_URL", + help_heading = HEADING_RPC, + )] + pub rpc_url: Option, + /// Network passphrase to sign the transaction sent to the rpc server + #[clap( + long = "network-passphrase", + requires = "rpc-url", + env = "SOROBAN_NETWORK_PASSPHRASE", + help_heading = HEADING_RPC, + )] + pub network_passphrase: Option, + /// Name of network to use from config + #[clap( + long, + conflicts_with = "network-passphrase", + conflicts_with = "rpc-url" + )] + pub network: Option, +} + +impl Args { + pub fn get_network(&self) -> Result { + if let Some(name) = self.network.as_deref() { + Ok(locator::read_network(name)?) + } else { + Ok(Network { + rpc_url: self.rpc_url.clone().unwrap(), + network_passphrase: self.network_passphrase.clone().unwrap(), + }) + } + } +} + +#[derive(Debug, clap::Args, Serialize, Deserialize)] +pub struct Network { + /// RPC server endpoint + #[clap( + long, + env = "SOROBAN_RPC_URL", + help_heading = HEADING_RPC, + )] + pub rpc_url: String, + /// Network passphrase to sign the transaction sent to the rpc server + #[clap( + long, + env = "SOROBAN_NETWORK_PASSPHRASE", + help_heading = HEADING_RPC, + )] + pub network_passphrase: String, +} diff --git a/cmd/soroban-cli/src/config/network/rm.rs b/cmd/soroban-cli/src/config/network/rm.rs new file mode 100644 index 000000000..c6d142650 --- /dev/null +++ b/cmd/soroban-cli/src/config/network/rm.rs @@ -0,0 +1,32 @@ +use crate::config::locator; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("No such network {name}")] + MissingNetwork { name: String }, + #[error("Error deleting {path}")] + DeletingIdFile { path: String }, +} + +#[derive(Debug, clap::Args)] +pub struct Cmd { + /// default name + pub default_name: String, + + #[clap(flatten)] + pub config_locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + let path = self + .config_locator + .network_path(&self.default_name) + .map_err(|_| Error::MissingNetwork { + name: self.default_name.clone(), + })?; + std::fs::remove_file(&path).map_err(|_| Error::DeletingIdFile { + path: format!("{}", path.display()), + }) + } +} diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs new file mode 100644 index 000000000..4b74b13f8 --- /dev/null +++ b/cmd/soroban-cli/src/config/secret.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; +use std::{io::Write, str::FromStr}; +use stellar_strkey::ed25519::PrivateKey; + +use crate::utils; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("invalid secret key")] + InvalidSecretKey, + // #[error("seed_phrase must be 12 words long, found {len}")] + // InvalidSeedPhrase { len: usize }, + #[error("seceret input error")] + PasswordRead, + #[error(transparent)] + Secret(#[from] stellar_strkey::DecodeError), + #[error(transparent)] + SeedPhrase(#[from] sep5::error::Error), + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), +} + +#[derive(Debug, clap::Args, Clone)] +pub struct Args { + /// Add using secret_key + #[clap(long, conflicts_with = "seed-phrase")] + pub secret_key: bool, + /// Add using 12 word seed phrase to generate secret_key + #[clap(long, conflicts_with = "secret-key")] + pub seed_phrase: bool, +} + +impl Args { + pub fn read_secret(&self) -> Result { + if self.secret_key { + println!("Type a secret key: "); + let secret_key = read_password()?; + let secret_key = PrivateKey::from_string(&secret_key) + .map_err(|_| Error::InvalidSecretKey)? + .to_string(); + Ok(Secret::SecretKey { secret_key }) + } else if self.seed_phrase { + println!("Type a 12 word seed phrase: "); + let seed_phrase = read_password()?; + let seed_phrase: Vec<&str> = seed_phrase.split_whitespace().collect(); + // if seed_phrase.len() != 12 { + // let len = seed_phrase.len(); + // return Err(Error::InvalidSeedPhrase { len }); + // } + Ok(Secret::SeedPhrase { + seed_phrase: seed_phrase + .into_iter() + .map(ToString::to_string) + .collect::>() + .join(" "), + }) + } else { + Err(Error::PasswordRead {}) + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Secret { + SecretKey { secret_key: String }, + SeedPhrase { seed_phrase: String }, +} + +impl Secret { + pub fn private_key(&self, index: Option) -> Result { + Ok(match self { + Secret::SecretKey { secret_key } => PrivateKey::from_string(secret_key)?, + Secret::SeedPhrase { seed_phrase } => sep5::SeedPhrase::from_str(seed_phrase)? + .from_path_index(index.unwrap_or_default(), None)? + .private(), + }) + } + + pub fn key_pair(&self, index: Option) -> Result { + Ok(utils::into_key_pair(&self.private_key(index)?)?) + } + + pub fn from_seed(seed: Option<&String>) -> Result { + let seed_phrase = if let Some(seed) = seed.map(String::as_bytes) { + sep5::SeedPhrase::from_entropy(seed) + } else { + sep5::SeedPhrase::random(sep5::MnemonicType::Words12) + }? + .seed_phrase + .into_phrase(); + Ok(Secret::SeedPhrase { seed_phrase }) + } +} + +fn read_password() -> Result { + std::io::stdout().flush().map_err(|_| Error::PasswordRead)?; + rpassword::read_password().map_err(|_| Error::PasswordRead) +} diff --git a/cmd/soroban-cli/src/contract/bindings.rs b/cmd/soroban-cli/src/contract/bindings.rs index 189d0c16d..deec84350 100644 --- a/cmd/soroban-cli/src/contract/bindings.rs +++ b/cmd/soroban-cli/src/contract/bindings.rs @@ -6,11 +6,12 @@ use soroban_spec::gen::{ rust::{self, ToFormattedString}, }; +use soroban_cli::wasm; + #[derive(Parser, Debug)] pub struct Cmd { - /// WASM file to generate code for - #[clap(long, parse(from_os_str))] - wasm: std::path::PathBuf, + #[clap(flatten)] + wasm: wasm::Args, /// Type of output to generate #[clap(long, arg_enum)] r#output: Output, @@ -43,7 +44,7 @@ impl Cmd { } pub fn generate_rust(&self) -> Result<(), Error> { - let wasm_path_str = self.wasm.to_string_lossy(); + let wasm_path_str = self.wasm.wasm.to_string_lossy(); let code = rust::generate_from_file(&wasm_path_str, None).map_err(Error::GenerateRustFromFile)?; match code.to_formatted_string() { @@ -59,7 +60,7 @@ impl Cmd { } pub fn generate_json(&self) -> Result<(), Error> { - let wasm_path_str = self.wasm.to_string_lossy(); + let wasm_path_str = self.wasm.wasm.to_string_lossy(); let json = json::generate_from_file(&wasm_path_str, None).map_err(Error::GenerateJsonFromFile)?; println!("{json}"); diff --git a/cmd/soroban-cli/src/contract/deploy.rs b/cmd/soroban-cli/src/contract/deploy.rs index c21639e39..1dc27a4e9 100644 --- a/cmd/soroban-cli/src/contract/deploy.rs +++ b/cmd/soroban-cli/src/contract/deploy.rs @@ -6,6 +6,7 @@ use clap::Parser; use hex::FromHexError; use rand::Rng; use sha2::{Digest, Sha256}; +use soroban_cli::wasm; use soroban_env_host::xdr::HashIdPreimageSourceAccountContractId; use soroban_env_host::xdr::{ AccountId, ContractId, CreateContractArgs, Error as XdrError, Hash, HashIdPreimage, @@ -16,6 +17,7 @@ use soroban_env_host::xdr::{ }; use soroban_env_host::HostError; +use crate::config; use crate::contract::install; use crate::rpc::{self, Client}; use crate::{utils, HEADING_RPC, HEADING_SANDBOX}; @@ -42,24 +44,6 @@ pub struct Cmd { help_heading = HEADING_SANDBOX, )] contract_id: Option, - /// File to persist ledger state - #[clap( - long, - parse(from_os_str), - default_value = ".soroban/ledger.json", - conflicts_with = "rpc-url", - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - ledger_file: std::path::PathBuf, - - /// Secret 'S' key used to sign the transaction sent to the rpc server - #[clap( - long = "secret-key", - env = "SOROBAN_SECRET_KEY", - help_heading = HEADING_RPC, - )] - secret_key: Option, /// Custom salt 32-byte salt for the token id #[clap( long, @@ -67,23 +51,8 @@ pub struct Cmd { help_heading = HEADING_RPC, )] salt: Option, - /// RPC server endpoint - #[clap( - long, - conflicts_with = "contract-id", - requires = "secret-key", - requires = "network-passphrase", - env = "SOROBAN_RPC_URL", - help_heading = HEADING_RPC, - )] - rpc_url: Option, - /// Network passphrase to sign the transaction sent to the rpc server - #[clap( - long = "network-passphrase", - env = "SOROBAN_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - network_passphrase: Option, + #[clap(flatten)] + config: config::Args, } #[derive(thiserror::Error, Debug)] @@ -102,16 +71,6 @@ pub enum Error { JsonRpc(#[from] jsonrpsee_core::Error), #[error("cannot parse salt: {salt}")] CannotParseSalt { salt: String }, - #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("committing file {filepath}: {error}")] - CannotCommitLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, #[error("cannot parse contract ID {contract_id}: {error}")] CannotParseContractId { contract_id: String, @@ -122,10 +81,10 @@ pub enum Error { wasm_hash: String, error: FromHexError, }, - #[error("cannot parse secret key")] - CannotParseSecretKey, #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] + Config(#[from] config::Error), } impl Cmd { @@ -138,11 +97,8 @@ impl Cmd { pub async fn run_and_get_contract_id(&self) -> Result { let wasm_hash = if let Some(wasm) = &self.wasm { install::Cmd { - wasm: wasm.clone(), - ledger_file: self.ledger_file.clone(), - secret_key: self.secret_key.clone(), - rpc_url: self.rpc_url.clone(), - network_passphrase: self.network_passphrase.clone(), + wasm: wasm::Args { wasm: wasm.clone() }, + config: self.config.clone(), } .run_and_get_hash() .await? @@ -158,10 +114,10 @@ impl Cmd { })?, ); - if self.rpc_url.is_some() { - self.run_against_rpc_server(hash).await - } else { + if self.config.is_no_network() { self.run_in_sandbox(hash) + } else { + self.run_against_rpc_server(hash).await } } @@ -175,25 +131,14 @@ impl Cmd { None => rand::thread_rng().gen::<[u8; 32]>(), }; - let mut state = utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { - Error::CannotReadLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - } - })?; + let mut state = self.config.get_state()?; utils::add_contract_to_ledger_entries(&mut state.ledger_entries, contract_id, wasm_hash.0); - - state - .write_file(&self.ledger_file) - .map_err(|e| Error::CannotCommitLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - })?; - + self.config.set_state(&mut state)?; Ok(hex::encode(contract_id)) } async fn run_against_rpc_server(&self, wasm_hash: Hash) -> Result { + let network = self.config.get_network()?; let salt: [u8; 32] = match &self.salt { // Hack: re-use contract_id_from_str to parse the 32-byte salt hex. Some(h) => { @@ -202,9 +147,8 @@ impl Cmd { None => rand::thread_rng().gen::<[u8; 32]>(), }; - let client = Client::new(self.rpc_url.as_ref().unwrap()); - let key = utils::parse_secret_key(self.secret_key.as_ref().unwrap()) - .map_err(|_| Error::CannotParseSecretKey)?; + let client = Client::new(&network.rpc_url); + let key = self.config.key_pair()?; // Get the account sequence number let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string(); @@ -217,7 +161,7 @@ impl Cmd { wasm_hash, sequence + 1, fee, - self.network_passphrase.as_ref().unwrap(), + &network.network_passphrase, salt, &key, )?; diff --git a/cmd/soroban-cli/src/contract/inspect.rs b/cmd/soroban-cli/src/contract/inspect.rs index d7ce7be35..d4ec57ca3 100644 --- a/cmd/soroban-cli/src/contract/inspect.rs +++ b/cmd/soroban-cli/src/contract/inspect.rs @@ -1,102 +1,24 @@ use clap::Parser; -use soroban_env_host::xdr::{Error as XdrError, ReadXdr, ScEnvMetaEntry, ScSpecEntry}; -use std::{ - fmt::Debug, - fs, - io::{self, Cursor}, -}; +use std::fmt::Debug; + +use soroban_cli::wasm; #[derive(Parser, Debug)] pub struct Cmd { - /// WASM file to inspect - #[clap(long, parse(from_os_str))] - wasm: std::path::PathBuf, + #[clap(flatten)] + wasm: wasm::Args, } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("reading file {filepath}: {error}")] - CannotReadContractFile { - filepath: std::path::PathBuf, - error: io::Error, - }, - #[error("cannot parse wasm file {file}: {error}")] - CannotParseWasm { - file: std::path::PathBuf, - error: wasmparser::BinaryReaderError, - }, - #[error("xdr processing error: {0}")] - Xdr(#[from] XdrError), + #[error(transparent)] + Wasm(#[from] wasm::Error), } impl Cmd { pub fn run(&self) -> Result<(), Error> { - println!("File: {}", self.wasm.to_string_lossy()); - - let contents = fs::read(&self.wasm).map_err(|e| Error::CannotReadContractFile { - filepath: self.wasm.clone(), - error: e, - })?; - - let mut env_meta: Option<&[u8]> = None; - let mut spec: Option<&[u8]> = None; - for payload in wasmparser::Parser::new(0).parse_all(&contents) { - let payload = payload.map_err(|e| Error::CannotParseWasm { - file: self.wasm.clone(), - error: e, - })?; - if let wasmparser::Payload::CustomSection(section) = payload { - let out = match section.name() { - "contractenvmetav0" => &mut env_meta, - "contractspecv0" => &mut spec, - _ => continue, - }; - *out = Some(section.data()); - }; - } - - if let Some(env_meta) = env_meta { - println!("Env Meta: {}", base64::encode(env_meta)); - let mut cursor = Cursor::new(env_meta); - for env_meta_entry in ScEnvMetaEntry::read_xdr_iter(&mut cursor) { - match env_meta_entry? { - ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) => { - println!(" • Interface Version: {v}"); - } - } - } - } else { - println!("Env Meta: None"); - } - - if let Some(spec) = spec { - println!("Contract Spec: {}", base64::encode(spec)); - let mut cursor = Cursor::new(spec); - for spec_entry in ScSpecEntry::read_xdr_iter(&mut cursor) { - match spec_entry? { - ScSpecEntry::FunctionV0(f) => println!( - " • Function: {} ({:?}) -> ({:?})", - f.name.to_string()?, - f.inputs.as_slice(), - f.outputs.as_slice(), - ), - ScSpecEntry::UdtUnionV0(udt) => { - println!(" • Union: {udt:?}"); - } - ScSpecEntry::UdtStructV0(udt) => { - println!(" • Struct: {udt:?}"); - } - ScSpecEntry::UdtEnumV0(udt) => { - println!(" • Enum: {udt:?}"); - } - ScSpecEntry::UdtErrorEnumV0(udt) => { - println!(" • Error: {udt:?}"); - } - } - } - } else { - println!("Contract Spec: None"); - } + println!("File: {}", self.wasm.wasm.to_string_lossy()); + print!("{}", self.wasm.parse()?); Ok(()) } } diff --git a/cmd/soroban-cli/src/contract/install.rs b/cmd/soroban-cli/src/contract/install.rs index 34e945267..905f60a8b 100644 --- a/cmd/soroban-cli/src/contract/install.rs +++ b/cmd/soroban-cli/src/contract/install.rs @@ -1,6 +1,6 @@ use std::array::TryFromSliceError; +use std::fmt::Debug; use std::num::ParseIntError; -use std::{fmt::Debug, fs, io}; use clap::Parser; use soroban_env_host::xdr::{ @@ -12,47 +12,15 @@ use soroban_env_host::xdr::{ use soroban_env_host::HostError; use crate::rpc::{self, Client}; -use crate::{utils, HEADING_RPC, HEADING_SANDBOX}; +use crate::{config, utils}; +use soroban_cli::wasm; #[derive(Parser, Debug)] pub struct Cmd { - /// WASM file to install - #[clap(long, parse(from_os_str))] - pub wasm: std::path::PathBuf, - /// File to persist ledger state - #[clap( - long, - parse(from_os_str), - default_value = ".soroban/ledger.json", - conflicts_with = "rpc-url", - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - pub ledger_file: std::path::PathBuf, - - /// Secret 'S' key used to sign the transaction sent to the rpc server - #[clap( - long = "secret-key", - env = "SOROBAN_SECRET_KEY", - help_heading = HEADING_RPC, - )] - pub secret_key: Option, - /// RPC server endpoint - #[clap( - long, - requires = "secret-key", - requires = "network-passphrase", - env = "SOROBAN_RPC_URL", - help_heading = HEADING_RPC, - )] - pub rpc_url: Option, - /// Network passphrase to sign the transaction sent to the rpc server - #[clap( - long = "network-passphrase", - env = "SOROBAN_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - pub network_passphrase: Option, + #[clap(flatten)] + pub wasm: wasm::Args, + #[clap(flatten)] + pub config: config::Args, } #[derive(thiserror::Error, Debug)] @@ -67,25 +35,12 @@ pub enum Error { Xdr(#[from] XdrError), #[error("jsonrpc error: {0}")] JsonRpc(#[from] jsonrpsee_core::Error), - #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("reading file {filepath}: {error}")] - CannotReadContractFile { - filepath: std::path::PathBuf, - error: io::Error, - }, - #[error("committing file {filepath}: {error}")] - CannotCommitLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("cannot parse secret key")] - CannotParseSecretKey, #[error(transparent)] Rpc(#[from] rpc::Error), + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Wasm(#[from] wasm::Error), } impl Cmd { @@ -96,42 +51,28 @@ impl Cmd { } pub async fn run_and_get_hash(&self) -> Result { - let contract = fs::read(&self.wasm).map_err(|e| Error::CannotReadContractFile { - filepath: self.wasm.clone(), - error: e, - })?; - - if self.rpc_url.is_some() { - self.run_against_rpc_server(contract).await - } else { + let contract = self.wasm.read()?; + if self.config.is_no_network() { self.run_in_sandbox(contract) + } else { + self.run_against_rpc_server(contract).await } } fn run_in_sandbox(&self, contract: Vec) -> Result { - let mut state = utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { - Error::CannotReadLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - } - })?; + let mut state = self.config.get_state()?; let wasm_hash = utils::add_contract_code_to_ledger_entries(&mut state.ledger_entries, contract)?; - state - .write_file(&self.ledger_file) - .map_err(|e| Error::CannotCommitLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - })?; + self.config.set_state(&mut state)?; Ok(hex::encode(wasm_hash)) } async fn run_against_rpc_server(&self, contract: Vec) -> Result { - let client = Client::new(self.rpc_url.as_ref().unwrap()); - let key = utils::parse_secret_key(self.secret_key.as_ref().unwrap()) - .map_err(|_| Error::CannotParseSecretKey)?; + let network = self.config.get_network()?; + let client = Client::new(&network.rpc_url); + let key = self.config.key_pair()?; // Get the account sequence number let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string(); @@ -144,7 +85,7 @@ impl Cmd { contract, sequence + 1, fee, - self.network_passphrase.as_ref().unwrap(), + &network.network_passphrase, &key, )?; client.send_transaction(&tx).await?; diff --git a/cmd/soroban-cli/src/contract/invoke.rs b/cmd/soroban-cli/src/contract/invoke.rs index 0f31c8444..dc6dcd3d5 100644 --- a/cmd/soroban-cli/src/contract/invoke.rs +++ b/cmd/soroban-cli/src/contract/invoke.rs @@ -25,11 +25,12 @@ use soroban_env_host::{ }; use soroban_spec::read::FromWasmError; +use crate::config; use crate::rpc::Client; use crate::strval::Spec; use crate::utils::{create_ledger_footprint, default_account_ledger_entry}; +use crate::HEADING_SANDBOX; use crate::{events, rpc, strval, utils}; -use crate::{HEADING_RPC, HEADING_SANDBOX}; #[derive(Parser, Debug)] pub struct Cmd { @@ -57,16 +58,6 @@ pub struct Cmd { help_heading = HEADING_SANDBOX, )] account_id: stellar_strkey::ed25519::PublicKey, - /// File to persist ledger state - #[clap( - long, - parse(from_os_str), - default_value(".soroban/ledger.json"), - conflicts_with = "rpc-url", - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - ledger_file: std::path::PathBuf, /// File to persist event output #[clap( long, @@ -78,36 +69,12 @@ pub struct Cmd { )] events_file: std::path::PathBuf, - /// Secret 'S' key used to sign the transaction sent to the rpc server - #[clap( - long = "secret-key", - requires = "rpc-url", - env = "SOROBAN_SECRET_KEY", - help_heading = HEADING_RPC, - )] - secret_key: Option, - /// RPC server endpoint - #[clap( - long, - conflicts_with = "account-id", - requires = "secret-key", - requires = "network-passphrase", - env = "SOROBAN_RPC_URL", - help_heading = HEADING_RPC, - )] - rpc_url: Option, - /// Network passphrase to sign the transaction sent to the rpc server - #[clap( - long = "network-passphrase", - requires = "rpc-url", - env = "SOROBAN_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - network_passphrase: Option, - // Arguments for contract as `--arg-name value`, `--arg-xdr-name base64-encoded-xdr` - #[clap(last = true, name = "ARGS")] + #[clap(last = true, name = "CONTRACT_FN_ARGS")] pub slop: Vec, + + #[clap(flatten)] + pub config: config::Args, } #[derive(thiserror::Error, Debug)] @@ -121,21 +88,11 @@ pub enum Error { // (it just calls Debug). I think we can do better than that Host(#[from] HostError), #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("reading file {filepath}: {error}")] CannotReadContractFile { filepath: std::path::PathBuf, error: io::Error, }, #[error("committing file {filepath}: {error}")] - CannotCommitLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("committing file {filepath}: {error}")] CannotCommitEventsFile { filepath: std::path::PathBuf, error: events::Error, @@ -165,8 +122,6 @@ pub enum Error { Xdr(#[from] XdrError), #[error("error parsing int: {0}")] ParseIntError(#[from] ParseIntError), - #[error("cannot parse secret key")] - CannotParseSecretKey, #[error(transparent)] Rpc(#[from] rpc::Error), #[error("unexpected contract code data type: {0:?}")] @@ -177,6 +132,9 @@ pub enum Error { // ArgsFile(std::path::PathBuf), #[error(transparent)] StrVal(#[from] strval::Error), + + #[error(transparent)] + Config(#[from] config::Error), } static INSTANCE: OnceCell> = OnceCell::new(); @@ -201,7 +159,7 @@ impl Cmd { let cmd = build_custom_cmd(&self.function, inputs_map, &spec)?; let matches_ = cmd.get_matches_from(&self.slop); - // let res = ; + let parsed_args = inputs_map .iter() .map(|(name, t)| { @@ -248,18 +206,18 @@ impl Cmd { } pub async fn run(&self) -> Result<(), Error> { - if self.rpc_url.is_some() { - self.run_against_rpc_server().await - } else { + if self.config.is_no_network() { self.run_in_sandbox() + } else { + self.run_against_rpc_server().await } } async fn run_against_rpc_server(&self) -> Result<(), Error> { let contract_id = self.contract_id()?; - let client = Client::new(self.rpc_url.as_ref().unwrap()); - let key = utils::parse_secret_key(self.secret_key.as_ref().unwrap()) - .map_err(|_| Error::CannotParseSecretKey)?; + let network = &self.config.get_network()?; + let client = Client::new(&network.rpc_url); + let key = self.config.key_pair()?; // Get the account sequence number let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string(); @@ -284,7 +242,7 @@ impl Cmd { None, sequence + 1, fee, - self.network_passphrase.as_ref().unwrap(), + &network.network_passphrase, &key, )?; let simulation_response = client.simulate_transaction(&tx_without_footprint).await?; @@ -300,7 +258,7 @@ impl Cmd { Some(footprint), sequence + 1, fee, - self.network_passphrase.as_ref().unwrap(), + &network.network_passphrase, &key, )?; @@ -320,12 +278,7 @@ impl Cmd { let contract_id = self.contract_id()?; // Initialize storage and host // TODO: allow option to separate input and output file - let mut state = utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { - Error::CannotReadLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - } - })?; + let mut state = self.config.get_state()?; // If a file is specified, deploy the contract to storage if let Some(contract) = self.read_wasm()? { @@ -410,12 +363,7 @@ impl Cmd { } } - state - .write_file(&self.ledger_file) - .map_err(|e| Error::CannotCommitLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - })?; + self.config.set_state(&mut state)?; events::commit(&events.0, &state, &self.events_file).map_err(|e| { Error::CannotCommitEventsFile { diff --git a/cmd/soroban-cli/src/contract/optimize.rs b/cmd/soroban-cli/src/contract/optimize.rs index c0d21cb28..79e529d27 100644 --- a/cmd/soroban-cli/src/contract/optimize.rs +++ b/cmd/soroban-cli/src/contract/optimize.rs @@ -2,11 +2,12 @@ use clap::Parser; use std::fmt::Debug; use wasm_opt::{OptimizationError, OptimizationOptions}; +use soroban_cli::wasm; + #[derive(Parser, Debug)] pub struct Cmd { - /// WASM file to optimize - #[clap(long, parse(from_os_str))] - wasm: std::path::PathBuf, + #[clap(flatten)] + wasm: wasm::Args, /// Path to write the optimized WASM file to (defaults to same location as --wasm with .optimized.wasm suffix) #[clap(long, parse(from_os_str))] wasm_out: Option, @@ -14,30 +15,28 @@ pub struct Cmd { #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("reading file: {0}")] - ReadingFile(std::io::Error), + #[error(transparent)] + Wasm(#[from] wasm::Error), #[error("optimization error: {0}")] OptimizationError(OptimizationError), } impl Cmd { pub fn run(&self) -> Result<(), Error> { - let wasm_size = std::fs::metadata(&self.wasm) - .map_err(Error::ReadingFile)? - .len(); + let wasm_size = self.wasm.len()?; println!( "Reading: {} ({} bytes)", - self.wasm.to_string_lossy(), + self.wasm.wasm.to_string_lossy(), wasm_size ); let wasm_out = self.wasm_out.as_ref().cloned().unwrap_or_else(|| { - let mut wasm_out = self.wasm.clone(); + let mut wasm_out = self.wasm.wasm.clone(); wasm_out.set_extension("optimized.wasm"); wasm_out }); - println!("Writing to: {}...", self.wasm.to_string_lossy()); + println!("Writing to: {}...", wasm_out.to_string_lossy()); let mut options = OptimizationOptions::new_optimize_for_size_aggressively(); options.converge = true; @@ -48,12 +47,10 @@ impl Cmd { options.mvp_features_only(); options - .run(&self.wasm, &wasm_out) + .run(&self.wasm.wasm, &wasm_out) .map_err(Error::OptimizationError)?; - let wasm_out_size = std::fs::metadata(&wasm_out) - .map_err(Error::ReadingFile)? - .len(); + let wasm_out_size = wasm::len(&wasm_out)?; println!( "Optimized: {} ({} bytes)", wasm_out.to_string_lossy(), diff --git a/cmd/soroban-cli/src/contract/read.rs b/cmd/soroban-cli/src/contract/read.rs index 5b9049561..ab47080ae 100644 --- a/cmd/soroban-cli/src/contract/read.rs +++ b/cmd/soroban-cli/src/contract/read.rs @@ -13,7 +13,7 @@ use soroban_env_host::{ HostError, }; -use crate::{strval, utils, HEADING_SANDBOX}; +use crate::{config::ledger_file, strval, utils}; #[derive(Parser, Debug)] pub struct Cmd { @@ -30,15 +30,8 @@ pub struct Cmd { #[clap(long, arg_enum, default_value("string"))] output: Output, - /// File to persist ledger state - #[clap( - long, - parse(from_os_str), - default_value(".soroban/ledger.json"), - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - ledger_file: std::path::PathBuf, + #[clap(flatten)] + ledger: ledger_file::Args, } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ArgEnum)] @@ -53,15 +46,12 @@ pub enum Output { #[derive(thiserror::Error, Debug)] pub enum Error { + #[error(transparent)] + Ledger(#[from] ledger_file::Error), #[error("parsing key {key}: {error}")] CannotParseKey { key: String, error: strval::Error }, #[error("parsing XDR key {key}: {error}")] CannotParseXdrKey { key: String, error: XdrError }, - #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, #[error("cannot parse contract ID {contract_id}: {error}")] CannotParseContractId { contract_id: String, @@ -114,12 +104,7 @@ impl Cmd { None }; - let state = utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { - Error::CannotReadLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - } - })?; + let state = self.ledger.read()?; let ledger_entries = &state.ledger_entries; let contract_id = xdr::Hash(contract_id); diff --git a/cmd/soroban-cli/src/lab/token/wrap.rs b/cmd/soroban-cli/src/lab/token/wrap.rs index 8a6168da3..560b89ae5 100644 --- a/cmd/soroban-cli/src/lab/token/wrap.rs +++ b/cmd/soroban-cli/src/lab/token/wrap.rs @@ -17,8 +17,9 @@ use soroban_env_host::{ use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError, rc::Rc}; use crate::{ + config, rpc::{Client, Error as SorobanRpcError}, - utils, HEADING_RPC, HEADING_SANDBOX, + utils, }; #[derive(thiserror::Error, Debug)] @@ -27,18 +28,6 @@ pub enum Error { CannotParseAccountId { account_id: String }, #[error("cannot parse asset: {asset}")] CannotParseAsset { asset: String }, - #[error("cannot parse secret key")] - CannotParseSecretKey, - #[error("reading file {filepath}: {error}")] - CannotReadLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, - #[error("committing file {filepath}: {error}")] - CannotCommitLedgerFile { - filepath: std::path::PathBuf, - error: soroban_ledger_snapshot::Error, - }, #[error(transparent)] // TODO: the Display impl of host errors is pretty user-unfriendly // (it just calls Debug). I think we can do better than that @@ -53,49 +42,18 @@ pub enum Error { TryFromSliceError(#[from] TryFromSliceError), #[error("xdr processing error: {0}")] Xdr(#[from] XdrError), + #[error(transparent)] + Config(#[from] config::Error), } #[derive(Parser, Debug)] pub struct Cmd { /// ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" #[clap(long)] - asset: String, - - /// File to persist ledger state (if using the sandbox) - #[clap( - long, - parse(from_os_str), - default_value = ".soroban/ledger.json", - conflicts_with = "rpc-url", - env = "SOROBAN_LEDGER_FILE", - help_heading = HEADING_SANDBOX, - )] - ledger_file: std::path::PathBuf, + pub asset: String, - /// Secret key to sign the transaction sent to the rpc server - #[clap( - long = "secret-key", - env = "SOROBAN_SECRET_KEY", - help_heading = HEADING_RPC, - )] - secret_key: Option, - /// RPC server endpoint - #[clap( - long, - conflicts_with = "ledger-file", - requires = "secret-key", - requires = "network-passphrase", - env = "SOROBAN_RPC_URL", - help_heading = HEADING_RPC, - )] - rpc_url: Option, - /// Network passphrase to sign the transaction sent to the rpc server - #[clap( - long = "network-passphrase", - env = "SOROBAN_NETWORK_PASSPHRASE", - help_heading = HEADING_RPC, - )] - network_passphrase: Option, + #[clap(flatten)] + pub config: config::Args, } impl Cmd { @@ -103,10 +61,10 @@ impl Cmd { // Parse asset let asset = parse_asset(&self.asset)?; - let res_str = if self.rpc_url.is_some() { - self.run_against_rpc_server(asset).await? - } else { + let res_str = if self.config.is_no_network() { self.run_in_sandbox(&asset)? + } else { + self.run_against_rpc_server(asset).await? }; println!("{res_str}"); Ok(()) @@ -115,12 +73,7 @@ impl Cmd { fn run_in_sandbox(&self, asset: &Asset) -> Result { // Initialize storage and host // TODO: allow option to separate input and output file - let mut state = utils::ledger_snapshot_read_or_default(&self.ledger_file).map_err(|e| { - Error::CannotReadLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - } - })?; + let mut state = self.config.get_state()?; let snap = Rc::new(state.clone()); let h = Host::with_storage_and_budget( @@ -140,19 +93,14 @@ impl Cmd { let res_str = utils::vec_to_hash(&res)?; state.update(&h); - state - .write_file(&self.ledger_file) - .map_err(|e| Error::CannotCommitLedgerFile { - filepath: self.ledger_file.clone(), - error: e, - })?; + self.config.set_state(&mut state)?; Ok(res_str) } async fn run_against_rpc_server(&self, asset: Asset) -> Result { - let client = Client::new(self.rpc_url.as_ref().unwrap()); - let key = utils::parse_secret_key(self.secret_key.as_ref().unwrap()) - .map_err(|_| Error::CannotParseSecretKey)?; + let network = self.config.get_network()?; + let client = Client::new(&network.rpc_url); + let key = self.config.key_pair()?; // Get the account sequence number let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string(); @@ -161,7 +109,7 @@ impl Cmd { // TODO: create a cmdline parameter for the fee instead of simply using the minimum fee let fee: u32 = 100; let sequence = account_details.sequence.parse::()?; - let network_passphrase = self.network_passphrase.as_ref().unwrap(); + let network_passphrase = &network.network_passphrase; let contract_id = get_contract_id(&asset, network_passphrase)?; let tx = build_wrap_token_tx( &asset, diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index c9913570a..8774d60e1 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -1,3 +1,4 @@ pub mod network; pub mod strval; pub mod utils; +pub mod wasm; diff --git a/cmd/soroban-cli/src/main.rs b/cmd/soroban-cli/src/main.rs index 7eb4844d1..304051469 100644 --- a/cmd/soroban-cli/src/main.rs +++ b/cmd/soroban-cli/src/main.rs @@ -1,6 +1,7 @@ use clap::{AppSettings, CommandFactory, FromArgMatches, Parser}; mod completion; +mod config; mod contract; mod events; mod jsonrpc; @@ -15,7 +16,6 @@ mod version; const HEADING_SANDBOX: &str = "OPTIONS (SANDBOX)"; const HEADING_RPC: &str = "OPTIONS (RPC)"; - #[derive(Parser, Debug)] #[clap( name = "soroban", @@ -35,6 +35,9 @@ enum Cmd { /// Tools for smart contract developers #[clap(subcommand)] Contract(contract::SubCmd), + /// Read and update config + #[clap(subcommand)] + Config(config::Cmd), /// Run a local webserver for web app development and testing Serve(serve::Cmd), /// Watch the network for contract events @@ -60,6 +63,8 @@ enum CmdError { Serve(#[from] serve::Error), #[error(transparent)] Lab(#[from] lab::Error), + #[error(transparent)] + Config(#[from] config::Error), } async fn run(cmd: Cmd) -> Result<(), CmdError> { @@ -70,6 +75,7 @@ async fn run(cmd: Cmd) -> Result<(), CmdError> { Cmd::Lab(lab) => lab.run().await?, Cmd::Version(version) => version.run(), Cmd::Completion(completion) => completion.run(), + Cmd::Config(config) => config.run()?, }; Ok(()) } diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 8eabd169d..8cb7567e8 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -18,6 +18,7 @@ use soroban_env_host::{ }; use soroban_ledger_snapshot::LedgerSnapshot; use soroban_spec::read::FromWasmError; +use stellar_strkey::ed25519::PrivateKey; use crate::network::SANDBOX_NETWORK_PASSPHRASE; @@ -219,27 +220,6 @@ pub fn vec_to_hash(res: &ScVal) -> Result { } } -#[derive(thiserror::Error, Debug)] -pub enum ParseSecretKeyError { - #[error("cannot parse secret key")] - CannotParseSecretKey, -} - -/// # Errors -/// -/// Might return an error -pub fn parse_secret_key(strkey: &str) -> Result { - let seed = stellar_strkey::ed25519::PrivateKey::from_string(strkey) - .map_err(|_| ParseSecretKeyError::CannotParseSecretKey)?; - let secret_key = ed25519_dalek::SecretKey::from_bytes(&seed.0) - .map_err(|_| ParseSecretKeyError::CannotParseSecretKey)?; - let public_key = (&secret_key).into(); - Ok(ed25519_dalek::Keypair { - secret: secret_key, - public: public_key, - }) -} - /// # Panics /// /// May panic @@ -288,27 +268,33 @@ pub fn default_account_ledger_entry(account_id: AccountId) -> LedgerEntry { } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_secret_key() { - let seed = "SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP"; - let keypair = parse_secret_key(seed).unwrap(); +/// # Errors +/// May not find a config dir +pub fn find_config_dir(mut pwd: std::path::PathBuf) -> std::io::Result { + let soroban_dir = |p: &std::path::Path| p.join(".soroban"); + while !soroban_dir(&pwd).exists() { + if !pwd.pop() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "soroban directory not found", + )); + } + } + Ok(soroban_dir(&pwd)) +} - let expected_public_key: [u8; 32] = [ - 0x31, 0x40, 0xf1, 0x40, 0x99, 0xa7, 0x4c, 0x90, 0xd4, 0x62, 0x48, 0xec, 0x8d, 0xef, - 0xb3, 0x38, 0xc8, 0x2c, 0xe2, 0x42, 0x85, 0xc9, 0xf7, 0xb8, 0x95, 0xce, 0xdd, 0x6f, - 0x96, 0x47, 0x82, 0x96, - ]; - assert_eq!(expected_public_key, keypair.public.to_bytes()); +pub(crate) fn into_key_pair( + key: &PrivateKey, +) -> Result { + let secret = ed25519_dalek::SecretKey::from_bytes(&key.0)?; + let public = (&secret).into(); + Ok(ed25519_dalek::Keypair { secret, public }) +} - let expected_secret_key: [u8; 32] = [ - 0x4a, 0x62, 0x97, 0x5f, 0xc7, 0xb9, 0x9a, 0x18, 0xa0, 0x41, 0xba, 0x6, 0x24, 0xd0, - 0x70, 0xf3, 0x95, 0x57, 0x58, 0x82, 0x81, 0x5a, 0x51, 0xbc, 0x3b, 0x49, 0xae, 0x5f, - 0x37, 0x1e, 0x9c, 0x4a, - ]; - assert_eq!(expected_secret_key, keypair.secret.to_bytes()); - } +/// Used in tests +#[allow(unused)] +pub(crate) fn parse_secret_key( + s: &str, +) -> Result { + into_key_pair(&PrivateKey::from_string(s).unwrap()) } diff --git a/cmd/soroban-cli/src/wasm.rs b/cmd/soroban-cli/src/wasm.rs new file mode 100644 index 000000000..925b4836d --- /dev/null +++ b/cmd/soroban-cli/src/wasm.rs @@ -0,0 +1,166 @@ +use std::{ + fmt::Display, + fs, + io::{self, Cursor}, + path::Path, +}; + +use soroban_env_host::xdr::{self, ReadXdr, ScEnvMetaEntry, ScSpecEntry}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("reading file {filepath}: {error}")] + CannotReadContractFile { + filepath: std::path::PathBuf, + error: io::Error, + }, + #[error("cannot parse wasm file {file}: {error}")] + CannotParseWasm { + file: std::path::PathBuf, + error: wasmparser::BinaryReaderError, + }, + #[error("xdr processing error: {0}")] + Xdr(#[from] xdr::Error), +} + +#[derive(Debug, clap::Args)] +pub struct Args { + /// Path to wasm binary + #[clap(long)] + pub wasm: std::path::PathBuf, +} + +impl Args { + /// # Errors + /// May fail to read wasm file + pub fn read(&self) -> Result, Error> { + fs::read(&self.wasm).map_err(|e| Error::CannotReadContractFile { + filepath: self.wasm.clone(), + error: e, + }) + } + + /// # Errors + /// May fail to read wasm file + pub fn len(&self) -> Result { + len(&self.wasm) + } + + /// # Errors + /// May fail to read wasm file + pub fn is_empty(&self) -> Result { + self.len().map(|len| len == 0) + } + + /// # Errors + /// May fail to read wasm file or parse xdr section + pub fn parse(&self) -> Result { + let contents = self.read()?; + let mut env_meta: Option<&[u8]> = None; + let mut spec: Option<&[u8]> = None; + for payload in wasmparser::Parser::new(0).parse_all(&contents) { + let payload = payload.map_err(|e| Error::CannotParseWasm { + file: self.wasm.clone(), + error: e, + })?; + if let wasmparser::Payload::CustomSection(section) = payload { + let out = match section.name() { + "contractenvmetav0" => &mut env_meta, + "contractspecv0" => &mut spec, + _ => continue, + }; + *out = Some(section.data()); + }; + } + + let mut env_meta_base64 = None; + let env_meta = if let Some(env_meta) = env_meta { + env_meta_base64 = Some(base64::encode(env_meta)); + let mut cursor = Cursor::new(env_meta); + ScEnvMetaEntry::read_xdr_iter(&mut cursor).collect::, xdr::Error>>()? + } else { + vec![] + }; + + let mut spec_base64 = None; + let spec = if let Some(spec) = spec { + spec_base64 = Some(base64::encode(spec)); + let mut cursor = Cursor::new(spec); + ScSpecEntry::read_xdr_iter(&mut cursor).collect::, xdr::Error>>()? + } else { + vec![] + }; + + Ok(ContractSpec { + env_meta_base64, + env_meta, + spec_base64, + spec, + }) + } +} + +pub struct ContractSpec { + pub env_meta_base64: Option, + pub env_meta: Vec, + pub spec_base64: Option, + pub spec: Vec, +} + +impl Display for ContractSpec { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(env_meta) = &self.env_meta_base64 { + writeln!(f, "Env Meta: {env_meta}")?; + for env_meta_entry in &self.env_meta { + match env_meta_entry { + ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) => { + writeln!(f, " • Interface Version: {v}")?; + } + } + } + } else { + writeln!(f, "Env Meta: None")?; + } + + if let Some(spec_base64) = &self.spec_base64 { + writeln!(f, "Contract Spec: {spec_base64}")?; + for spec_entry in &self.spec { + match spec_entry { + ScSpecEntry::FunctionV0(func) => writeln!( + f, + " • Function: {} ({:?}) -> ({:?})", + func.name.to_string_lossy(), + func.inputs.as_slice(), + func.outputs.as_slice(), + )?, + ScSpecEntry::UdtUnionV0(udt) => { + writeln!(f, " • Union: {udt:?}")?; + } + ScSpecEntry::UdtStructV0(udt) => { + writeln!(f, " • Struct: {udt:?}")?; + } + ScSpecEntry::UdtEnumV0(udt) => { + writeln!(f, " • Enum: {udt:?}")?; + } + ScSpecEntry::UdtErrorEnumV0(udt) => { + writeln!(f, " • Error: {udt:?}")?; + } + } + } + } else { + writeln!(f, "Contract Spec: None")?; + } + Ok(()) + } +} + +/// # Errors +/// May fail to read wasm file +pub fn len(p: &Path) -> Result { + Ok(std::fs::metadata(p) + .map_err(|e| Error::CannotReadContractFile { + filepath: p.to_path_buf(), + error: e, + })? + .len()) +} diff --git a/cmd/soroban-cli/tests/fixtures/args/world b/cmd/soroban-cli/tests/fixtures/args/world new file mode 100644 index 000000000..04fea0642 --- /dev/null +++ b/cmd/soroban-cli/tests/fixtures/args/world @@ -0,0 +1 @@ +world \ No newline at end of file diff --git a/cmd/soroban-cli/tests/it/config.rs b/cmd/soroban-cli/tests/it/config.rs new file mode 100644 index 000000000..940094c6c --- /dev/null +++ b/cmd/soroban-cli/tests/it/config.rs @@ -0,0 +1,232 @@ +use assert_cmd::Command; + +use crate::util::{ + add_identity, add_test_id, temp_dir, temp_ledger_file, Sandbox, SecretKind, HELLO_WORLD, +}; +use std::{fs, path::Path}; + +#[test] +fn set_and_remove_network() { + let sandbox = Sandbox::new(); + sandbox + .new_cmd("config") + .arg("network") + .arg("add") + .arg("--rpc-url") + .arg("https://127.0.0.1") + .arg("local") + .arg("--network-passphrase") + .arg("Local Sandbox Stellar Network ; September 2022") + .assert() + .success(); + let dir = &sandbox.temp_dir; + let file = std::fs::read_dir(dir.join(".soroban/networks")) + .unwrap() + .next() + .unwrap() + .unwrap(); + assert_eq!(file.file_name().to_str().unwrap(), "local.toml"); + + sandbox + .new_cmd("config") + .arg("network") + .arg("ls") + .assert() + .stdout("local\n"); + + sandbox + .new_cmd("config") + .arg("network") + .arg("rm") + .arg("local") + .assert() + .stdout(""); + sandbox + .new_cmd("config") + .arg("network") + .arg("ls") + .assert() + .stdout("\n"); +} + +fn add_network(sandbox: &Sandbox, name: &str) -> Command { + let mut cmd = sandbox.new_cmd("config"); + cmd.arg("network") + .arg("add") + .arg("--rpc-url") + .arg("https://127.0.0.1") + .arg("--network-passphrase") + .arg("Local Sandbox Stellar Network ; September 2022") + .arg(name); + cmd +} + +fn add_network_global(sandbox: &Sandbox, dir: &Path, name: &str) { + sandbox + .new_cmd("config") + .env("XDG_CONFIG_HOME", dir.to_str().unwrap()) + .arg("network") + .arg("add") + .arg("--global") + .arg("--rpc-url") + .arg("https://127.0.0.1") + .arg("--network-passphrase") + .arg("Local Sandbox Stellar Network ; September 2022") + .arg(name) + .assert() + .success(); +} + +#[test] +fn set_and_remove_global_network() { + let sandbox = Sandbox::new(); + let dir = temp_dir(); + + add_network_global(&sandbox, &dir, "global"); + + sandbox + .new_cmd("config") + .env("XDG_CONFIG_HOME", dir.to_str().unwrap()) + .arg("network") + .arg("ls") + .arg("--global") + .assert() + .stdout("global\n"); + + sandbox + .new_cmd("config") + .env("XDG_CONFIG_HOME", dir.to_str().unwrap()) + .arg("network") + .arg("rm") + .arg("--global") + .arg("global") + .assert() + .stdout(""); + + sandbox + .new_cmd("config") + .env("XDG_CONFIG_HOME", dir.to_str().unwrap()) + .arg("network") + .arg("ls") + .assert() + .stdout("\n"); +} + +#[test] +fn mulitple_networks() { + let sandbox = Sandbox::new(); + + add_network(&sandbox, "local").assert().success(); + add_network(&sandbox, "local2").assert().success(); + + sandbox + .new_cmd("config") + .arg("network") + .arg("ls") + .assert() + .stdout("local\nlocal2\n"); + + sandbox + .new_cmd("config") + .arg("network") + .arg("rm") + .arg("local") + .assert(); + sandbox + .new_cmd("config") + .arg("network") + .arg("ls") + .assert() + .stdout("local2\n"); + + let sub_dir = sandbox.dir().join("sub_directory"); + fs::create_dir(&sub_dir).unwrap(); + add_network(&sandbox, "local3\n") + .current_dir(sub_dir) + .assert() + .success(); + + sandbox + .new_cmd("config") + .arg("network") + .arg("ls") + .assert() + .stdout("local2\nlocal3\n"); +} + +#[test] +fn read_identity() { + let sandbox = Sandbox::new(); + add_test_id(&sandbox.temp_dir); + sandbox + .new_cmd("config") + .arg("identity") + .arg("ls") + .assert() + .stdout("test_id\n"); +} + +#[test] +fn generate_identity() { + let sandbox = Sandbox::new(); + let seed_phrase = + "coral light army gather adapt blossom school alcohol coral light army giggle"; + sandbox + .new_cmd("config") + .arg("identity") + .arg("generate") + .arg("--seed") + .arg("0000000000000000") + .arg("mike") + .assert() + .success(); + sandbox + .new_cmd("config") + .arg("identity") + .arg("ls") + .assert() + .stdout("mike\n"); + let file_contents = + fs::read_to_string(sandbox.dir().join(".soroban/identities/mike.toml")).unwrap(); + assert_eq!(file_contents, format!("seed_phrase = \"{seed_phrase}\"\n")); +} + +#[test] +fn seed_phrase() { + let sandbox = Sandbox::new(); + let dir = &sandbox.temp_dir; + add_identity( + dir, + "test_seed", + SecretKind::Seed, + "one two three four five six seven eight nine ten eleven twelve", + ); + + sandbox + .new_cmd("config") + .current_dir(dir) + .arg("identity") + .arg("ls") + .assert() + .stdout("test_seed\n"); +} + +#[test] +fn use_different_ledger_file() { + let sandbox = Sandbox::new(); + sandbox + .new_cmd("contract") + .arg("invoke") + .arg("--id=1") + .arg("--wasm") + .arg(HELLO_WORLD.path()) + .arg("--ledger-file") + .arg(temp_ledger_file()) + .arg("--fn=hello") + .arg("--") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); + assert!(fs::read(sandbox.dir().join(".soroban/ledger.json")).is_err()); +} diff --git a/cmd/soroban-cli/tests/it/custom_types.rs b/cmd/soroban-cli/tests/it/custom_types.rs index d276e108a..392f70164 100644 --- a/cmd/soroban-cli/tests/it/custom_types.rs +++ b/cmd/soroban-cli/tests/it/custom_types.rs @@ -1,39 +1,10 @@ -use std::fmt::Display; - -use assert_cmd::Command; use serde_json::json; -use crate::util::{temp_ledger_file, CommandExt, Sandbox, SorobanCommand, CUSTOM_TYPES}; - -fn invoke(func: &str) -> Command { - let mut s = Sandbox::new_cmd("contract"); - s.arg("invoke") - .arg("--ledger-file") - .arg(temp_ledger_file()) - .arg("--id=1") - .arg("--wasm") - .arg(CUSTOM_TYPES.path()) - .arg("--fn") - .arg(func) - .arg("--"); - s -} - -fn invoke_with_roundtrip(func: &str, data: D) -where - D: Display, -{ - invoke(func) - .arg(&format!("--{func}")) - .json_arg(&data) - .assert() - .success() - .stdout(format!("{data}\n")); -} +use crate::util::{invoke, invoke_with_roundtrip, Sandbox}; #[test] fn symbol() { - invoke("hello") + invoke(&Sandbox::new(), "hello") .arg("--hello") .arg("world") .assert() @@ -51,7 +22,10 @@ fn symbol_with_quotes() { #[test] fn generate_help() { - invoke("test").arg("--help").assert().success(); + invoke(&Sandbox::new(), "test") + .arg("--help") + .assert() + .success(); } #[test] @@ -110,7 +84,7 @@ fn const_enum() { #[test] fn boolean() { - invoke("boolean") + invoke(&Sandbox::new(), "boolean") .arg("--boolean") .assert() .success() @@ -121,23 +95,30 @@ fn boolean() { } #[test] fn boolean_no_flag() { - invoke("boolean").assert().success().stdout( - r#"false + invoke(&Sandbox::new(), "boolean") + .assert() + .success() + .stdout( + r#"false "#, - ); + ); } #[test] fn boolean_not() { - invoke("not").arg("--boolean").assert().success().stdout( - r#"false + invoke(&Sandbox::new(), "not") + .arg("--boolean") + .assert() + .success() + .stdout( + r#"false "#, - ); + ); } #[test] fn boolean_not_no_flag() { - invoke("not").assert().success().stdout( + invoke(&Sandbox::new(), "not").assert().success().stdout( r#"true "#, ); diff --git a/cmd/soroban-cli/tests/it/invoke_sandbox.rs b/cmd/soroban-cli/tests/it/invoke_sandbox.rs index c7ee0e417..2fc0bb60a 100644 --- a/cmd/soroban-cli/tests/it/invoke_sandbox.rs +++ b/cmd/soroban-cli/tests/it/invoke_sandbox.rs @@ -1,11 +1,10 @@ -use crate::util::{temp_ledger_file, Sandbox, SorobanCommand, HELLO_WORLD, INVOKER_ACCOUNT_EXISTS}; +use crate::util::{add_test_seed, Sandbox, HELLO_WORLD, INVOKER_ACCOUNT_EXISTS}; #[test] fn source_account_exists() { - Sandbox::new_cmd("contract") + Sandbox::new() + .new_cmd("contract") .arg("invoke") - .arg("--ledger-file") - .arg(temp_ledger_file()) .arg("--id=1") .arg("--wasm") .arg(INVOKER_ACCOUNT_EXISTS.path()) @@ -17,22 +16,20 @@ fn source_account_exists() { #[test] fn install_wasm_then_deploy_contract() { - let ledger = temp_ledger_file(); let hash = HELLO_WORLD.hash(); - Sandbox::new_cmd("contract") + let sandbox = Sandbox::new(); + sandbox + .new_cmd("contract") .arg("install") - .arg("--ledger-file") - .arg(&ledger) .arg("--wasm") .arg(HELLO_WORLD.path()) .assert() .success() .stdout(format!("{hash}\n")); - Sandbox::new_cmd("contract") + sandbox + .new_cmd("contract") .arg("deploy") - .arg("--ledger-file") - .arg(&ledger) .arg("--wasm-hash") .arg(&format!("{hash}")) .arg("--id=1") @@ -43,10 +40,9 @@ fn install_wasm_then_deploy_contract() { #[test] fn deploy_contract_with_wasm_file() { - Sandbox::new_cmd("contract") + Sandbox::new() + .new_cmd("contract") .arg("deploy") - .arg("--ledger-file") - .arg(temp_ledger_file()) .arg("--wasm") .arg(HELLO_WORLD.path()) .arg("--id=1") @@ -57,23 +53,22 @@ fn deploy_contract_with_wasm_file() { #[test] fn invoke_hello_world_with_deploy_first() { - // This test assumes a fresh standalone network rpc server on port 8000 - let ledger = temp_ledger_file(); - let res = Sandbox::new_cmd("contract") + let sandbox = Sandbox::new(); + let res = sandbox + .new_cmd("contract") .arg("deploy") .arg("--wasm") .arg(HELLO_WORLD.path()) - .arg("--ledger-file") - .arg(&ledger) .assert() .success(); let stdout = String::from_utf8(res.get_output().stdout.clone()).unwrap(); let id = stdout.trim_end(); - Sandbox::new_cmd("contract") + sandbox + .new_cmd("contract") .arg("invoke") - .arg("--ledger-file") - .arg(&ledger) + .arg("--identity") + .arg("test_id") .arg("--id") .arg(id) .arg("--fn=hello") @@ -86,15 +81,33 @@ fn invoke_hello_world_with_deploy_first() { #[test] fn invoke_hello_world() { - // This test assumes a fresh standalone network rpc server on port 8000 - let ledger = temp_ledger_file(); - Sandbox::new_cmd("contract") + let sandbox = Sandbox::new(); + sandbox + .new_cmd("contract") .arg("invoke") .arg("--id=1") .arg("--wasm") .arg(HELLO_WORLD.path()) - .arg("--ledger-file") - .arg(&ledger) + .arg("--fn=hello") + .arg("--") + .arg("--world=world") + .assert() + .stdout("[\"Hello\",\"world\"]\n") + .success(); +} + +#[test] +fn invoke_hello_world_with_seed() { + let sandbox = Sandbox::new(); + let identity = add_test_seed(sandbox.dir()); + sandbox + .new_cmd("contract") + .arg("invoke") + .arg("--identity") + .arg(identity) + .arg("--id=1") + .arg("--wasm") + .arg(HELLO_WORLD.path()) .arg("--fn=hello") .arg("--") .arg("--world=world") diff --git a/cmd/soroban-cli/tests/it/main.rs b/cmd/soroban-cli/tests/it/main.rs index 9ed636662..cc4fb4701 100644 --- a/cmd/soroban-cli/tests/it/main.rs +++ b/cmd/soroban-cli/tests/it/main.rs @@ -1,4 +1,5 @@ mod arg_parsing; +mod config; mod custom_types; mod invoke_sandbox; mod util; diff --git a/cmd/soroban-cli/tests/it/util.rs b/cmd/soroban-cli/tests/it/util.rs index 048421542..4f9aec3aa 100644 --- a/cmd/soroban-cli/tests/it/util.rs +++ b/cmd/soroban-cli/tests/it/util.rs @@ -18,7 +18,7 @@ impl Wasm<'_> { .join(self.0); path.set_extension("wasm"); assert!(path.is_file(), "File not found: {}. run 'make build-test-wasms' to generate .wasm files before running this test", path.display()); - path + std::env::current_dir().unwrap().join(path) } pub fn bytes(&self) -> Vec { @@ -30,20 +30,28 @@ impl Wasm<'_> { } } -/// Create a command with the correct env variables -pub trait SorobanCommand { - /// Default is with none - fn new_cmd(name: &str) -> Command { +/// Default +pub struct Sandbox { + pub temp_dir: TempDir, +} + +impl Sandbox { + pub fn new() -> Sandbox { + Self { + temp_dir: TempDir::new().expect("failed to create temp dir"), + } + } + pub fn new_cmd(&self, name: &str) -> Command { let mut this = Command::cargo_bin("soroban").expect("failed to find local soroban binary"); this.arg(name); + this.current_dir(&self.temp_dir); this } -} - -/// Default -pub struct Sandbox {} -impl SorobanCommand for Sandbox {} + pub fn dir(&self) -> &TempDir { + &self.temp_dir + } +} pub fn temp_ledger_file() -> OsString { TempDir::new() @@ -54,11 +62,11 @@ pub fn temp_ledger_file() -> OsString { } pub trait AssertExt { - fn output_line(&self) -> String; + fn stdout_as_str(&self) -> String; } impl AssertExt for Assert { - fn output_line(&self) -> String { + fn stdout_as_str(&self) -> String { String::from_utf8(self.get_output().stdout.clone()) .expect("failed to make str") .trim() @@ -92,3 +100,75 @@ pub fn contract_hash(contract: &[u8]) -> Result { pub const HELLO_WORLD: &Wasm = &Wasm("test_hello_world"); pub const INVOKER_ACCOUNT_EXISTS: &Wasm = &Wasm("test_invoker_account_exists"); pub const CUSTOM_TYPES: &Wasm = &Wasm("test_custom_types"); + +#[allow(unused)] +pub fn temp_dir() -> TempDir { + TempDir::new().unwrap() +} + +#[derive(Clone)] +pub enum SecretKind { + Seed, + Key, +} + +#[allow(clippy::needless_pass_by_value)] +pub fn add_identity(dir: &TempDir, name: &str, kind: SecretKind, data: &str) { + let identity_dir = dir.join(".soroban").join("identities"); + fs::create_dir_all(&identity_dir).unwrap(); + let kind_str = match kind { + SecretKind::Seed => "seed", + SecretKind::Key => "secret_key", + }; + fs::write( + identity_dir.join(format!("{name}.toml")), + format!("{kind_str} = {data}\n"), + ) + .unwrap(); +} + +pub fn add_test_id(dir: &TempDir) -> String { + let name = "test_id"; + add_identity( + dir, + name, + SecretKind::Key, + "SBGWSG6BTNCKCOB3DIFBGCVMUPQFYPA2G4O34RMTB343OYPXU5DJDVMN", + ); + name.to_owned() +} + +pub fn add_test_seed(dir: &TempDir) -> String { + let name = "test_seed"; + add_identity( + dir, + name, + SecretKind::Seed, + "one two three four five six seven eight nine ten eleven twelve", + ); + name.to_owned() +} + +pub fn invoke(sandbox: &Sandbox, func: &str) -> Command { + let mut s = sandbox.new_cmd("contract"); + s.arg("invoke") + .arg("--id=1") + .arg("--wasm") + .arg(CUSTOM_TYPES.path()) + .arg("--fn") + .arg(func) + .arg("--"); + s +} + +pub fn invoke_with_roundtrip(func: &str, data: D) +where + D: Display, +{ + invoke(&Sandbox::new(), func) + .arg(&format!("--{func}")) + .json_arg(&data) + .assert() + .success() + .stdout(format!("{data}\n")); +}