From aad9be9708037e122e8e9ff09a67a09c26168513 Mon Sep 17 00:00:00 2001 From: Gleb Date: Sat, 28 Sep 2024 01:55:49 -0700 Subject: [PATCH 1/8] Code cleanup (#1635) --- cmd/crates/soroban-spec-tools/src/utils.rs | 264 --------------------- 1 file changed, 264 deletions(-) diff --git a/cmd/crates/soroban-spec-tools/src/utils.rs b/cmd/crates/soroban-spec-tools/src/utils.rs index 66b153a11..f030cc0f8 100644 --- a/cmd/crates/soroban-spec-tools/src/utils.rs +++ b/cmd/crates/soroban-spec-tools/src/utils.rs @@ -1,268 +1,4 @@ -use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; use hex::FromHexError; -use std::{ - fmt::Display, - io::{self, Cursor}, -}; - -use stellar_xdr::curr::{ - Limited, Limits, ReadXdr, ScEnvMetaEntry, ScMetaEntry, ScMetaV0, ScSpecEntry, ScSpecFunctionV0, - ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0, ScSpecUdtStructV0, ScSpecUdtUnionV0, StringM, -}; - -pub struct ContractSpec { - pub env_meta_base64: Option, - pub env_meta: Vec, - pub meta_base64: Option, - pub meta: Vec, - pub spec_base64: Option, - pub spec: Vec, -} - -#[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] stellar_xdr::curr::Error), - - #[error(transparent)] - Parser(#[from] wasmparser::BinaryReaderError), -} - -impl ContractSpec { - pub fn new(bytes: &[u8]) -> Result { - let mut env_meta: Option<&[u8]> = None; - let mut meta: Option<&[u8]> = None; - let mut spec: Option<&[u8]> = None; - for payload in wasmparser::Parser::new(0).parse_all(bytes) { - let payload = payload?; - if let wasmparser::Payload::CustomSection(section) = payload { - let out = match section.name() { - "contractenvmetav0" => &mut env_meta, - "contractmetav0" => &mut 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 cursor = Cursor::new(env_meta); - let mut read = Limited::new(cursor, Limits::none()); - ScEnvMetaEntry::read_xdr_iter(&mut read).collect::, _>>()? - } else { - vec![] - }; - - let mut meta_base64 = None; - let meta = if let Some(meta) = meta { - meta_base64 = Some(base64.encode(meta)); - let cursor = Cursor::new(meta); - let mut read = Limited::new(cursor, Limits::none()); - ScMetaEntry::read_xdr_iter(&mut read).collect::, _>>()? - } else { - vec![] - }; - - let mut spec_base64 = None; - let spec = if let Some(spec) = spec { - spec_base64 = Some(base64.encode(spec)); - let cursor = Cursor::new(spec); - let mut read = Limited::new(cursor, Limits::none()); - ScSpecEntry::read_xdr_iter(&mut read).collect::, _>>()? - } else { - vec![] - }; - - Ok(ContractSpec { - env_meta_base64, - env_meta, - meta_base64, - meta, - spec_base64, - spec, - }) - } -} - -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}")?; - } - } - } - writeln!(f)?; - } else { - writeln!(f, "Env Meta: None\n")?; - } - - if let Some(_meta) = &self.meta_base64 { - writeln!(f, "Contract Meta:")?; - for meta_entry in &self.meta { - match meta_entry { - ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) => { - writeln!(f, " • {key}: {val}")?; - } - } - } - writeln!(f)?; - } else { - writeln!(f, "Contract Meta: None\n")?; - } - - if let Some(_spec_base64) = &self.spec_base64 { - writeln!(f, "Contract Spec:")?; - for spec_entry in &self.spec { - match spec_entry { - ScSpecEntry::FunctionV0(func) => write_func(f, func)?, - ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?, - ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?, - ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?, - ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?, - } - } - } else { - writeln!(f, "Contract Spec: None")?; - } - Ok(()) - } -} - -fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result { - writeln!(f, " • Function: {}", func.name.to_utf8_string_lossy())?; - if func.doc.len() > 0 { - writeln!( - f, - " Docs: {}", - &indent(&func.doc.to_utf8_string_lossy(), 11).trim() - )?; - } - writeln!( - f, - " Inputs: {}", - indent(&format!("{:#?}", func.inputs), 5).trim() - )?; - writeln!( - f, - " Output: {}", - indent(&format!("{:#?}", func.outputs), 5).trim() - )?; - writeln!(f)?; - Ok(()) -} - -fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result { - writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?; - if udt.doc.len() > 0 { - writeln!( - f, - " Docs: {}", - indent(&udt.doc.to_utf8_string_lossy(), 10).trim() - )?; - } - writeln!(f, " Cases:")?; - for case in udt.cases.iter() { - writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?; - } - writeln!(f)?; - Ok(()) -} - -fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result { - writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?; - if udt.doc.len() > 0 { - writeln!( - f, - " Docs: {}", - indent(&udt.doc.to_utf8_string_lossy(), 10).trim() - )?; - } - writeln!(f, " Fields:")?; - for field in udt.fields.iter() { - writeln!( - f, - " • {}: {}", - field.name.to_utf8_string_lossy(), - indent(&format!("{:#?}", field.type_), 8).trim() - )?; - if field.doc.len() > 0 { - writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?; - } - } - writeln!(f)?; - Ok(()) -} - -fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result { - writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?; - if udt.doc.len() > 0 { - writeln!( - f, - " Docs: {}", - indent(&udt.doc.to_utf8_string_lossy(), 10).trim() - )?; - } - writeln!(f, " Cases:")?; - for case in udt.cases.iter() { - writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?; - } - writeln!(f)?; - Ok(()) -} - -fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result { - writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?; - if udt.doc.len() > 0 { - writeln!( - f, - " Docs: {}", - indent(&udt.doc.to_utf8_string_lossy(), 10).trim() - )?; - } - writeln!(f, " Cases:")?; - for case in udt.cases.iter() { - writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?; - } - writeln!(f)?; - Ok(()) -} - -fn indent(s: &str, n: usize) -> String { - let pad = " ".repeat(n); - s.lines() - .map(|line| format!("{pad}{line}")) - .collect::>() - .join("\n") -} - -fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String { - if lib.len() > 0 { - format!( - "{}::{}", - lib.to_utf8_string_lossy(), - name.to_utf8_string_lossy() - ) - } else { - name.to_utf8_string_lossy() - } -} /// # Errors /// From 1f20dcdd7ed3dd0325d1d51238eda72173c38570 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 2 Oct 2024 02:07:15 -0400 Subject: [PATCH 2/8] feat: add `tx::builder` module and initial classic commands (#1551) --- Cargo.lock | 8 + FULL_HELP_DOCS.md | 283 +++++++- cmd/crates/soroban-test/Cargo.toml | 1 + cmd/crates/soroban-test/src/lib.rs | 19 +- .../tests/it/integration/cookbook.rs | 29 +- .../tests/it/integration/hello_world.rs | 10 +- .../soroban-test/tests/it/integration/tx.rs | 10 +- .../tests/it/integration/tx/operations.rs | 616 ++++++++++++++++++ .../soroban-test/tests/it/integration/util.rs | 39 +- .../soroban-test/tests/it/integration/wrap.rs | 4 +- cmd/crates/soroban-test/tests/it/util.rs | 3 +- cmd/soroban-cli/Cargo.toml | 1 + .../src/commands/contract/alias/add.rs | 2 +- .../src/commands/contract/deploy/asset.rs | 38 +- .../src/commands/contract/deploy/wasm.rs | 42 +- .../src/commands/contract/extend.rs | 12 +- .../src/commands/contract/id/asset.rs | 9 +- .../src/commands/contract/id/wasm.rs | 27 +- .../src/commands/contract/install.rs | 30 +- .../src/commands/contract/restore.rs | 12 +- .../src/commands/snapshot/create.rs | 18 +- cmd/soroban-cli/src/commands/tx/args.rs | 107 +++ cmd/soroban-cli/src/commands/tx/mod.rs | 18 +- .../src/commands/tx/new/account_merge.rs | 19 + .../src/commands/tx/new/bump_sequence.rs | 21 + .../src/commands/tx/new/change_trust.rs | 29 + .../src/commands/tx/new/create_account.rs | 25 + .../src/commands/tx/new/manage_data.rs | 29 + cmd/soroban-cli/src/commands/tx/new/mod.rs | 69 ++ .../src/commands/tx/new/payment.rs | 29 + .../src/commands/tx/new/set_options.rs | 123 ++++ .../commands/tx/new/set_trustline_flags.rs | 71 ++ cmd/soroban-cli/src/config/address.rs | 63 ++ cmd/soroban-cli/src/config/locator.rs | 8 +- cmd/soroban-cli/src/config/mod.rs | 45 +- cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/tx.rs | 4 + cmd/soroban-cli/src/tx/builder.rs | 11 + cmd/soroban-cli/src/tx/builder/asset.rs | 50 ++ cmd/soroban-cli/src/tx/builder/transaction.rs | 53 ++ cmd/soroban-cli/src/utils.rs | 111 +--- 41 files changed, 1830 insertions(+), 269 deletions(-) create mode 100644 cmd/crates/soroban-test/tests/it/integration/tx/operations.rs create mode 100644 cmd/soroban-cli/src/commands/tx/args.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/account_merge.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/change_trust.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/create_account.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/manage_data.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/mod.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/payment.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/set_options.rs create mode 100644 cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs create mode 100644 cmd/soroban-cli/src/config/address.rs create mode 100644 cmd/soroban-cli/src/tx.rs create mode 100644 cmd/soroban-cli/src/tx/builder.rs create mode 100644 cmd/soroban-cli/src/tx/builder/asset.rs create mode 100644 cmd/soroban-cli/src/tx/builder/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index cd3761b51..21df05973 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1560,6 +1560,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fqdn" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb540cf7bc4fe6df9d8f7f0c974cfd0dce8ed4e9e8884e73433b503ee78b4e7d" + [[package]] name = "fs_extra" version = "1.3.0" @@ -5119,6 +5125,7 @@ dependencies = [ "ed25519-dalek 2.1.1", "ethnum", "flate2", + "fqdn", "futures", "futures-util", "gix", @@ -5409,6 +5416,7 @@ dependencies = [ "assert_fs", "ed25519-dalek 2.1.1", "fs_extra", + "hex", "predicates 2.1.5", "sep5", "serde_json", diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index 479ac6720..be20ae24a 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -119,7 +119,7 @@ Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id a * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -138,7 +138,7 @@ Deploy builtin Soroban Asset Contract * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -363,7 +363,7 @@ If no keys are specified the contract itself is extended. * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -391,7 +391,7 @@ Deploy a wasm contract * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -452,7 +452,7 @@ Deploy builtin Soroban Asset Contract * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -471,7 +471,7 @@ Deploy normal Wasm Contract * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -669,7 +669,7 @@ 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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -708,7 +708,7 @@ stellar contract invoke ... -- --help * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -785,7 +785,7 @@ 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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -822,7 +822,7 @@ 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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -1293,6 +1293,7 @@ Sign, Simulate, and Send transactions * `hash` — Calculate the hash of a transaction envelope from stdin * `sign` — Sign a transaction envelope appending the signature to the envelope * `send` — Send a transaction envelope to the network +* `new` — Create a new transaction @@ -1307,7 +1308,7 @@ Simulate a transaction envelope from stdin * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail * `--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 ` — Location of config directory, default is "." @@ -1363,6 +1364,266 @@ Send a transaction envelope to the network +## `stellar tx new` + +Create a new transaction + +**Usage:** `stellar tx new ` + +###### **Subcommands:** + +* `account-merge` — Transfers the XLM balance of an account to another account and removes the source account from the ledger +* `bump-sequence` — Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number +* `change-trust` — Creates, updates, or deletes a trustline Learn more about trustlines https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines +* `create-account` — Creates and funds a new account with the specified starting balance +* `manage-data` — Sets, modifies, or deletes a data entry (name/value pair) that is attached to an account Learn more about entries and subentries: https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#subentries +* `payment` — Sends an amount in a specific asset to a destination account +* `set-options` — Set option for an account such as flags, inflation destination, signers, home domain, and master key weight Learn more about flags: https://developers.stellar.org/docs/learn/glossary#flags Learn more about the home domain: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md Learn more about signers operations and key weight: https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig +* `set-trustline-flags` — Allows issuing account to configure authorization and trustline flags to an asset The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the Asset type. If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's unique ID. Learn more about flags: https://developers.stellar.org/docs/learn/glossary#flags + + + +## `stellar tx new account-merge` + +Transfers the XLM balance of an account to another account and removes the source account from the ledger + +**Usage:** `stellar tx new account-merge [OPTIONS] --source-account --account ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--account ` — Muxed Account to merge with, e.g. `GBX...`, 'MBX...' + + + +## `stellar tx new bump-sequence` + +Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number + +**Usage:** `stellar tx new bump-sequence [OPTIONS] --source-account --bump-to ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--bump-to ` — Sequence number to bump to + + + +## `stellar tx new change-trust` + +Creates, updates, or deletes a trustline Learn more about trustlines https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines + +**Usage:** `stellar tx new change-trust [OPTIONS] --source-account --line ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--line ` +* `--limit ` — Limit for the trust line, 0 to remove the trust line + + Default value: `18446744073709551615` + + + +## `stellar tx new create-account` + +Creates and funds a new account with the specified starting balance + +**Usage:** `stellar tx new create-account [OPTIONS] --source-account --destination ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--destination ` — Account Id to create, e.g. `GBX...` +* `--starting-balance ` — Initial balance in stroops of the account, default 1 XLM + + Default value: `10_000_000` + + + +## `stellar tx new manage-data` + +Sets, modifies, or deletes a data entry (name/value pair) that is attached to an account Learn more about entries and subentries: https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#subentries + +**Usage:** `stellar tx new manage-data [OPTIONS] --source-account --data-name ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--data-name ` — Line to change, either 4 or 12 alphanumeric characters, or "native" if not specified +* `--data-value ` — Up to 64 bytes long hex string If not present then the existing Name will be deleted. If present then this value will be set in the `DataEntry` + + + +## `stellar tx new payment` + +Sends an amount in a specific asset to a destination account + +**Usage:** `stellar tx new payment [OPTIONS] --source-account --destination --amount ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--destination ` — Account to send to, e.g. `GBX...` +* `--asset ` — Asset to send, default native, e.i. XLM + + Default value: `native` +* `--amount ` — Amount of the aforementioned asset to send + + + +## `stellar tx new set-options` + +Set option for an account such as flags, inflation destination, signers, home domain, and master key weight Learn more about flags: https://developers.stellar.org/docs/learn/glossary#flags Learn more about the home domain: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md Learn more about signers operations and key weight: https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig + +**Usage:** `stellar tx new set-options [OPTIONS] --source-account ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--inflation-dest ` — Account of the inflation destination +* `--master-weight ` — A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled +* `--low-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a low threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig +* `--med-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a medium threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig +* `--high-threshold ` — A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a high threshold. https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig +* `--home-domain ` — Sets the home domain of an account. See https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/federation +* `--signer ` — Add, update, or remove a signer from an account +* `--signer-weight ` — Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0 +* `--set-required` — When enabled, an issuer must approve an account before that account can hold its asset. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-required-0x1 +* `--set-revocable` — When enabled, an issuer can revoke an existing trustline’s authorization, thereby freezing the asset held by an account. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-revocable-0x2 +* `--set-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. https://developers.stellar.org/docs/tokens/control-asset-access#clawback-enabled-0x8 +* `--set-immutable` — With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can’t be merged. https://developers.stellar.org/docs/tokens/control-asset-access#authorization-immutable-0x4 +* `--clear-required` +* `--clear-revocable` +* `--clear-immutable` +* `--clear-clawback-enabled` + + + +## `stellar tx new set-trustline-flags` + +Allows issuing account to configure authorization and trustline flags to an asset The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the Asset type. If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's unique ID. Learn more about flags: https://developers.stellar.org/docs/learn/glossary#flags + +**Usage:** `stellar tx new set-trustline-flags [OPTIONS] --source-account --trustor --asset ` + +###### **Options:** + +* `--fee ` — fee amount for transaction, in stroops. 1 stroop = 0.0000001 xlm + + Default value: `100` +* `--cost` — Output the cost execution to stderr +* `--instructions ` — Number of instructions to simulate +* `--build-only` — Build the transaction and only write the base64 xdr to stdout +* `--sim-only` — Simulate the transaction and only write the base64 xdr to stdout +* `--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 +* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail +* `--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 ` — Location of config directory, default is "." +* `--trustor ` — Account to set trustline flags for +* `--asset ` — Asset to set trustline flags for +* `--set-authorize` — Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders +* `--set-authorize-to-maintain-liabilities` — Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset +* `--set-trustline-clawback-enabled` — Enables the issuing account to take back (burning) all of the asset. See our section on Clawbacks: https://developers.stellar.org/docs/learn/encyclopedia/transactions-specialized/clawbacks +* `--clear-authorize` +* `--clear-authorize-to-maintain-liabilities` +* `--clear-trustline-clawback-enabled` + + + ## `stellar xdr` Decode and encode XDR diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index 09126adcf..a34b7c65c 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -43,6 +43,7 @@ tokio = "1.28.1" walkdir = "2.4.0" ulid.workspace = true ed25519-dalek = { workspace = true } +hex = { workspace = true } [features] it = [] diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index 544e2d59e..849b78796 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -59,6 +59,7 @@ pub enum Error { pub struct TestEnv { pub temp_dir: TempDir, pub rpc_url: String, + pub network_passphrase: String, } impl Default for TestEnv { @@ -67,6 +68,7 @@ impl Default for TestEnv { Self { temp_dir, rpc_url: "http://localhost:8889/soroban/rpc".to_string(), + network_passphrase: LOCAL_NETWORK_PASSPHRASE.to_string(), } } } @@ -100,10 +102,13 @@ impl TestEnv { } pub fn with_rpc_url(rpc_url: &str) -> TestEnv { - let env = TestEnv { + let mut env = TestEnv { rpc_url: rpc_url.to_string(), ..Default::default() }; + if let Ok(network_passphrase) = std::env::var("STELLAR_NETWORK_PASSPHRASE") { + env.network_passphrase = network_passphrase; + }; env.generate_account("test", None).assert().success(); env } @@ -112,6 +117,9 @@ impl TestEnv { if let Ok(rpc_url) = std::env::var("SOROBAN_RPC_URL") { return Self::with_rpc_url(&rpc_url); } + if let Ok(rpc_url) = std::env::var("STELLAR_RPC_URL") { + return Self::with_rpc_url(&rpc_url); + } let host_port = std::env::var("SOROBAN_PORT") .as_deref() .ok() @@ -193,7 +201,7 @@ impl TestEnv { command_str: &[I], source: &str, ) -> Result { - let cmd = self.cmd_with_config::(command_str); + let cmd = self.cmd_with_config::(command_str, None); self.run_cmd_with(cmd, source) .await .map(|r| r.into_result().unwrap()) @@ -203,12 +211,15 @@ impl TestEnv { pub fn cmd_with_config, T: CommandParser + NetworkRunnable>( &self, command_str: &[I], + source_account: Option<&str>, ) -> T { + let source = source_account.unwrap_or("test"); + let source_str = format!("--source-account={source}"); let mut arg = vec![ "--network=local", "--rpc-url=http", "--network-passphrase=AA", - "--source-account=test", + source_str.as_str(), ]; let input = command_str .iter() @@ -227,7 +238,7 @@ impl TestEnv { network_passphrase: Some(LOCAL_NETWORK_PASSPHRASE.to_string()), network: None, }, - source_account: account.to_string(), + source_account: account.parse().unwrap(), locator: config::locator::Args { global: false, config_dir, diff --git a/cmd/crates/soroban-test/tests/it/integration/cookbook.rs b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs index 65855d775..4c0ad5a63 100644 --- a/cmd/crates/soroban-test/tests/it/integration/cookbook.rs +++ b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs @@ -12,7 +12,8 @@ fn parse_command(command: &str) -> Vec { .collect() } -async fn run_command( +#[allow(clippy::too_many_lines, clippy::too_many_arguments)] +fn run_command( sandbox: &TestEnv, command: &str, wasm_path: &str, @@ -133,7 +134,8 @@ async fn run_command( Ok(()) } -async fn test_mdx_file( +#[allow(clippy::too_many_arguments)] +fn test_mdx_file( sandbox: &TestEnv, file_path: &str, wasm_path: &str, @@ -145,7 +147,7 @@ async fn test_mdx_file( key_xdr: &str, ) -> Result<(), String> { let content = fs::read_to_string(file_path) - .map_err(|e| format!("Failed to read file {}: {}", file_path, e))?; + .map_err(|e| format!("Failed to read file {file_path}: {e}"))?; let commands: Vec<&str> = content .split("```bash") @@ -153,7 +155,7 @@ async fn test_mdx_file( .filter_map(|block| block.split("```").next()) .collect(); - println!("Testing commands from file: {}", file_path); + println!("Testing commands from file: {file_path}"); for (i, command) in commands.iter().enumerate() { println!("Running command {}: {}", i + 1, command); @@ -167,8 +169,7 @@ async fn test_mdx_file( bob_id, native_id, key_xdr, - ) - .await?; + )?; } Ok(()) @@ -260,9 +261,7 @@ mod tests { let key_xdr = read_xdr.split(',').next().unwrap_or("").trim(); let repo_root = get_repo_root(); let docs_dir = repo_root.join("cookbook"); - if !docs_dir.is_dir() { - panic!("docs directory not found"); - } + assert!(docs_dir.is_dir(), "docs directory not found"); for entry in fs::read_dir(docs_dir).expect("Failed to read docs directory") { let entry = entry.expect("Failed to read directory entry"); @@ -273,18 +272,16 @@ mod tests { match test_mdx_file( &sandbox, file_path, - &wasm_path.to_str().unwrap(), + wasm_path.to_str().unwrap(), &wasm_hash, source, &contract_id, &bob_id, &native_id, - &key_xdr, - ) - .await - { - Ok(_) => println!("Successfully tested all commands in {}", file_path), - Err(e) => panic!("Error testing {}: {}", file_path, e), + key_xdr, + ) { + Ok(()) => println!("Successfully tested all commands in {file_path}"), + Err(e) => panic!("Error testing {file_path}: {e}"), } } } diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index c982b0c40..767c9a07c 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -4,7 +4,7 @@ use soroban_cli::{ contract::{self, fetch}, txn_result::TxnResult, }, - config::{locator, secret}, + config::{address::Address, locator, secret}, }; use soroban_rpc::GetLatestLedgerResponse; use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; @@ -20,7 +20,7 @@ async fn invoke_view_with_non_existent_source_account() { let id = deploy_hello(sandbox).await; let world = "world"; let mut cmd = hello_world_cmd(&id, world); - cmd.config.source_account = String::new(); + cmd.config.source_account = Address::default(); cmd.is_view = true; let res = sandbox.run_cmd_with(cmd, "test").await.unwrap(); assert_eq!(res, TxnResult::Res(format!(r#"["Hello",{world:?}]"#))); @@ -94,7 +94,7 @@ async fn invoke() { sandbox .new_assert_cmd("events") .arg("--start-ledger") - .arg(&sequence.to_string()) + .arg(sequence.to_string()) .arg("--id") .arg(id) .assert() @@ -144,7 +144,7 @@ async fn invoke() { invoke_log(sandbox, id); } -fn invoke_hello_world(sandbox: &TestEnv, id: &str) { +pub(crate) fn invoke_hello_world(sandbox: &TestEnv, id: &str) { sandbox .new_assert_cmd("contract") .arg("invoke") @@ -324,7 +324,7 @@ async fn half_max_instructions() { .arg("--fee") .arg("1000000") .arg("--instructions") - .arg(&(u32::MAX / 2).to_string()) + .arg((u32::MAX / 2).to_string()) .arg("--wasm") .arg(wasm.path()) .arg("--ignore-checks") diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index ef2d1e26c..66c4b69fe 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -3,11 +3,15 @@ use soroban_test::{AssertExt, TestEnv}; use crate::integration::util::{deploy_contract, DeployKind, HELLO_WORLD}; +mod operations; + #[tokio::test] async fn simulate() { let sandbox = &TestEnv::new(); - let xdr_base64_build_only = deploy_contract(sandbox, HELLO_WORLD, DeployKind::BuildOnly).await; - let xdr_base64_sim_only = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await; + let xdr_base64_build_only = + deploy_contract(sandbox, HELLO_WORLD, DeployKind::BuildOnly, None).await; + let xdr_base64_sim_only = + deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; let tx_env = TransactionEnvelope::from_xdr_base64(&xdr_base64_build_only, Limits::none()).unwrap(); let tx = soroban_cli::commands::tx::xdr::unwrap_envelope_v1(tx_env).unwrap(); @@ -60,7 +64,7 @@ async fn build_simulate_sign_send() { .assert() .success(); - let tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly).await; + let tx_simulated = deploy_contract(sandbox, HELLO_WORLD, DeployKind::SimOnly, None).await; dbg!("{tx_simulated}"); let tx_signed = sandbox diff --git a/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs new file mode 100644 index 000000000..9df808b34 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/tx/operations.rs @@ -0,0 +1,616 @@ +use soroban_cli::{ + tx::{builder, ONE_XLM}, + utils::contract_id_hash_from_asset, +}; +use soroban_sdk::xdr::{self, ReadXdr, SequenceNumber}; +use soroban_test::{AssertExt, TestEnv}; + +use crate::integration::{ + hello_world::invoke_hello_world, + util::{deploy_contract, DeployKind, HELLO_WORLD}, +}; + +fn test_address(sandbox: &TestEnv) -> String { + sandbox + .new_assert_cmd("keys") + .arg("address") + .arg("test") + .assert() + .success() + .stdout_as_str() +} + +fn new_account(sandbox: &TestEnv, name: &str) -> String { + sandbox.generate_account(name, None).assert().success(); + sandbox + .new_assert_cmd("keys") + .args(["address", name]) + .assert() + .success() + .stdout_as_str() +} + +// returns test and test1 addresses +fn setup_accounts(sandbox: &TestEnv) -> (String, String) { + (test_address(sandbox), new_account(sandbox, "test1")) +} + +#[tokio::test] +async fn create_account() { + let sandbox = &TestEnv::new(); + sandbox + .new_assert_cmd("keys") + .args(["generate", "--no-fund", "new"]) + .assert() + .success(); + + let address = sandbox + .new_assert_cmd("keys") + .args(["address", "new"]) + .assert() + .success() + .stdout_as_str(); + let test = test_address(sandbox); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let test_account = client.get_account(&test).await.unwrap(); + println!("test account has a balance of {}", test_account.balance); + let starting_balance = ONE_XLM * 100; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "create-account", + "--destination", + address.as_str(), + "--starting-balance", + starting_balance.to_string().as_str(), + ]) + .assert() + .success(); + let test_account_after = client.get_account(&test).await.unwrap(); + assert!(test_account_after.balance < test_account.balance); + let id = deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, Some("new")).await; + println!("{id}"); + invoke_hello_world(sandbox, &id); +} + +#[tokio::test] +async fn payment() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let (test, test1) = setup_accounts(sandbox); + let test_account = client.get_account(&test).await.unwrap(); + println!("test account has a balance of {}", test_account.balance); + + let before = client.get_account(&test).await.unwrap(); + let test1_account_entry_before = client.get_account(&test1).await.unwrap(); + + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--destination", + test1.as_str(), + "--amount", + ONE_XLM.to_string().as_str(), + ]) + .assert() + .success(); + let test1_account_entry = client.get_account(&test1).await.unwrap(); + assert_eq!( + ONE_XLM, + test1_account_entry.balance - test1_account_entry_before.balance, + "Should have One XLM more" + ); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(before.balance - 10_000_100, after.balance); +} + +#[tokio::test] +async fn bump_sequence() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let test = test_address(sandbox); + let before = client.get_account(&test).await.unwrap(); + let amount = 50; + let seq = SequenceNumber(before.seq_num.0 + amount); + // bump sequence tx new + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "bump-sequence", + "--bump-to", + seq.0.to_string().as_str(), + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(seq, after.seq_num); +} + +#[tokio::test] +async fn account_merge() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let (test, test1) = setup_accounts(sandbox); + let before = client.get_account(&test).await.unwrap(); + let before1 = client.get_account(&test1).await.unwrap(); + let fee = 100; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "account-merge", + "--source", + "test1", + "--account", + test.as_str(), + "--fee", + fee.to_string().as_str(), + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert!(client.get_account(&test1).await.is_err()); + assert_eq!(before.balance + before1.balance - fee, after.balance); +} + +#[tokio::test] +async fn set_trustline_flags() { + let sandbox = &TestEnv::new(); + let (test, issuer) = setup_accounts(sandbox); + let asset = format!("usdc:{issuer}"); + issue_asset(sandbox, &test, &asset, 100_000, 100).await; + sandbox + .new_assert_cmd("contract") + .arg("asset") + .arg("deploy") + .arg("--asset") + .arg(&asset) + .assert() + .success(); + let id = contract_id_hash_from_asset( + asset.parse::().unwrap(), + &sandbox.network_passphrase, + ); + // sandbox + // .new_assert_cmd("contract") + // .args([ + // "invoke", + // "--id", + // &id.to_string(), + // "--", + // "authorized", + // "--id", + // &test, + // ]) + // .assert() + // .success() + // .stdout("false\n"); + + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id.to_string(), + "--", + "authorized", + "--id", + &test, + ]) + .assert() + .success() + .stdout("true\n"); +} + +#[tokio::test] +async fn set_options_add_signer() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let (test, test1) = setup_accounts(sandbox); + let before = client.get_account(&test).await.unwrap(); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-options", + "--signer", + test1.as_str(), + "--signer-weight", + "1", + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(before.signers.len() + 1, after.signers.len()); + assert_eq!(after.signers.first().unwrap().key, test1.parse().unwrap()); + let key = xdr::LedgerKey::Account(xdr::LedgerKeyAccount { + account_id: test.parse().unwrap(), + }); + let res = client.get_ledger_entries(&[key]).await.unwrap(); + let xdr_str = res.entries.unwrap().clone().first().unwrap().clone().xdr; + let entry = xdr::LedgerEntryData::from_xdr_base64(&xdr_str, xdr::Limits::none()).unwrap(); + let xdr::LedgerEntryData::Account(xdr::AccountEntry { signers, .. }) = entry else { + panic!(); + }; + assert_eq!(signers.first().unwrap().key, test1.parse().unwrap()); + + // Now remove signer with a weight of 0 + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-options", + "--signer", + test1.as_str(), + "--signer-weight", + "0", + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(before.signers.len(), after.signers.len()); +} + +fn build_and_run(sandbox: &TestEnv, cmd: &str, args: &[&str]) -> String { + let mut args_2 = args.to_vec(); + args_2.push("--build-only"); + let res = sandbox + .new_assert_cmd(cmd) + .args(args_2) + .assert() + .success() + .stdout_as_str(); + sandbox.new_assert_cmd(cmd).args(args).assert().success(); + res +} + +#[tokio::test] +async fn set_options() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let (test, alice) = setup_accounts(sandbox); + let before = client.get_account(&test).await.unwrap(); + assert!(before.inflation_dest.is_none()); + let tx_xdr = build_and_run( + sandbox, + "tx", + &[ + "new", + "set-options", + "--inflation-dest", + alice.as_str(), + "--home-domain", + "test.com", + "--master-weight=100", + "--med-threshold=100", + "--low-threshold=100", + "--high-threshold=100", + "--signer", + alice.as_str(), + "--signer-weight=100", + "--set-required", + "--set-revocable", + "--set-clawback-enabled", + "--set-immutable", + ], + ); + println!("{tx_xdr}"); + let after = client.get_account(&test).await.unwrap(); + println!("{before:#?}\n{after:#?}"); + assert_eq!( + after.flags, + xdr::AccountFlags::ClawbackEnabledFlag as u32 + | xdr::AccountFlags::ImmutableFlag as u32 + | xdr::AccountFlags::RevocableFlag as u32 + | xdr::AccountFlags::RequiredFlag as u32 + ); + assert_eq!([100, 100, 100, 100], after.thresholds.0); + assert_eq!(100, after.signers[0].weight); + assert_eq!(alice, after.signers[0].key.to_string()); + let xdr::PublicKey::PublicKeyTypeEd25519(xdr::Uint256(key)) = after.inflation_dest.unwrap().0; + assert_eq!(alice, stellar_strkey::ed25519::PublicKey(key).to_string()); + assert_eq!("test.com", after.home_domain.to_string()); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-options", + "--inflation-dest", + test.as_str(), + "--home-domain", + "test.com", + "--master-weight=100", + "--med-threshold=100", + "--low-threshold=100", + "--high-threshold=100", + "--signer", + alice.as_str(), + "--signer-weight=100", + "--set-required", + "--set-revocable", + "--set-clawback-enabled", + ]) + .assert() + .failure(); +} + +#[tokio::test] +async fn set_some_options() { + let sandbox = &TestEnv::new(); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let test = test_address(sandbox); + let before = client.get_account(&test).await.unwrap(); + assert!(before.inflation_dest.is_none()); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-options", + "--set-clawback-enabled", + "--master-weight=100", + ]) + .assert() + .failure() + .stderr(predicates::str::contains("AuthRevocableRequired")); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-options", + "--set-revocable", + "--master-weight=100", + ]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(after.flags, xdr::AccountFlags::RevocableFlag as u32); + assert_eq!([100, 0, 0, 0], after.thresholds.0); + assert!(after.inflation_dest.is_none()); + assert_eq!( + after.home_domain, + "".parse::>().unwrap().into() + ); + assert!(after.signers.is_empty()); + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--set-clawback-enabled"]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!( + after.flags, + xdr::AccountFlags::RevocableFlag as u32 | xdr::AccountFlags::ClawbackEnabledFlag as u32 + ); + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--clear-clawback-enabled"]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(after.flags, xdr::AccountFlags::RevocableFlag as u32); + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--clear-revocable"]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(after.flags, 0); + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--set-required"]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(after.flags, xdr::AccountFlags::RequiredFlag as u32); + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--clear-required"]) + .assert() + .success(); + let after = client.get_account(&test).await.unwrap(); + assert_eq!(after.flags, 0); +} + +#[tokio::test] +async fn change_trust() { + let sandbox = &TestEnv::new(); + let (test, issuer) = setup_accounts(sandbox); + let asset = &format!("usdc:{issuer}"); + + let limit = 100_000_000; + let half_limit = limit / 2; + issue_asset(sandbox, &test, asset, limit, half_limit).await; + sandbox + .new_assert_cmd("contract") + .arg("asset") + .arg("deploy") + .arg("--asset") + .arg(asset) + .assert() + .success(); + + // wrap_cmd(&asset).run().await.unwrap(); + let id = contract_id_hash_from_asset( + asset.parse::().unwrap(), + &sandbox.network_passphrase, + ); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id.to_string(), + "--", + "balance", + "--id", + &test, + ]) + .assert() + .stdout(format!("\"{half_limit}\"\n")); + + let bob = new_account(sandbox, "bob"); + let bobs_limit = half_limit / 2; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "change-trust", + "--source=bob", + "--line", + asset, + "--limit", + bobs_limit.to_string().as_str(), + ]) + .assert() + .success(); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--destination", + &bob, + "--asset", + asset, + "--amount", + half_limit.to_string().as_str(), + ]) + .assert() + .failure(); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--destination", + &bob, + "--asset", + asset, + "--amount", + bobs_limit.to_string().as_str(), + ]) + .assert() + .success(); + sandbox + .new_assert_cmd("contract") + .args([ + "invoke", + "--id", + &id.to_string(), + "--", + "balance", + "--id", + &bob, + ]) + .assert() + .stdout(format!("\"{bobs_limit}\"\n")); +} + +#[tokio::test] +async fn manage_data() { + let sandbox = &TestEnv::new(); + let (test, _) = setup_accounts(sandbox); + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let key = "test"; + let value = "beefface"; + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "manage-data", + "--data-name", + key, + "--data-value", + value, + ]) + .assert() + .success(); + let account_id = xdr::AccountId(xdr::PublicKey::PublicKeyTypeEd25519(xdr::Uint256( + stellar_strkey::ed25519::PublicKey::from_string(&test) + .unwrap() + .0, + ))); + let orig_data_name: xdr::StringM<64> = key.parse().unwrap(); + let res = client + .get_ledger_entries(&[xdr::LedgerKey::Data(xdr::LedgerKeyData { + account_id, + data_name: orig_data_name.clone().into(), + })]) + .await + .unwrap(); + let value_res = res.entries.as_ref().unwrap().first().unwrap(); + let ledeger_entry_data = + xdr::LedgerEntryData::from_xdr_base64(&value_res.xdr, xdr::Limits::none()).unwrap(); + let xdr::LedgerEntryData::Data(xdr::DataEntry { + data_value, + data_name, + .. + }) = ledeger_entry_data + else { + panic!("Expected DataEntry"); + }; + assert_eq!(data_name, orig_data_name.into()); + assert_eq!(hex::encode(data_value.0.to_vec()), value); +} + +async fn issue_asset(sandbox: &TestEnv, test: &str, asset: &str, limit: u64, initial_balance: u64) { + let client = soroban_rpc::Client::new(&sandbox.rpc_url).unwrap(); + let test_before = client.get_account(test).await.unwrap(); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "change-trust", + "--line", + asset, + "--limit", + limit.to_string().as_str(), + ]) + .assert() + .success(); + + sandbox + .new_assert_cmd("tx") + .args(["new", "set-options", "--set-required"]) + .assert() + .success(); + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "set-trustline-flags", + "--asset", + asset, + "--trustor", + test, + "--set-authorize", + "--source", + "test1", + ]) + .assert() + .success(); + + let after = client.get_account(test).await.unwrap(); + assert_eq!(test_before.num_sub_entries + 1, after.num_sub_entries); + println!("aa"); + // Send a payment to the issuer + sandbox + .new_assert_cmd("tx") + .args([ + "new", + "payment", + "--destination", + test, + "--asset", + asset, + "--amount", + initial_balance.to_string().as_str(), + "--source=test1", + ]) + .assert() + .success(); +} diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index dec7941f6..22ef4fa82 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -40,37 +40,44 @@ impl Display for DeployKind { } pub async fn deploy_hello(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal).await + deploy_contract(sandbox, HELLO_WORLD, DeployKind::Normal, None).await } pub async fn deploy_custom(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_TYPES, DeployKind::Normal).await + deploy_contract(sandbox, CUSTOM_TYPES, DeployKind::Normal, None).await } pub async fn deploy_swap(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, SWAP, DeployKind::Normal).await + deploy_contract(sandbox, SWAP, DeployKind::Normal, None).await } pub async fn deploy_custom_account(sandbox: &TestEnv) -> String { - deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployKind::Normal).await + deploy_contract(sandbox, CUSTOM_ACCOUNT, DeployKind::Normal, None).await } pub async fn deploy_contract( sandbox: &TestEnv, wasm: &Wasm<'static>, deploy: DeployKind, + deployer: Option<&str>, ) -> String { - let cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>(&[ - "--fee", - "1000000", - "--wasm", - &wasm.path().to_string_lossy(), - "--salt", - TEST_SALT, - "--ignore-checks", - deploy.to_string().as_str(), - ]); - let res = sandbox.run_cmd_with(cmd, "test").await.unwrap(); + let cmd = sandbox.cmd_with_config::<_, commands::contract::deploy::wasm::Cmd>( + &[ + "--fee", + "1000000", + "--wasm", + &wasm.path().to_string_lossy(), + "--salt", + TEST_SALT, + "--ignore-checks", + &deploy.to_string(), + ], + None, + ); + let res = sandbox + .run_cmd_with(cmd, deployer.unwrap_or("test")) + .await + .unwrap(); match deploy { DeployKind::BuildOnly | DeployKind::SimOnly => match res.to_envelope() { commands::txn_result::TxnEnvelopeResult::TxnEnvelope(e) => { @@ -80,7 +87,7 @@ pub async fn deploy_contract( }, DeployKind::Normal => (), } - res.into_result().unwrap() + res.into_result().unwrap().to_string() } pub async fn extend_contract(sandbox: &TestEnv, id: &str) { diff --git a/cmd/crates/soroban-test/tests/it/integration/wrap.rs b/cmd/crates/soroban-test/tests/it/integration/wrap.rs index c43f5b007..3fef56ef9 100644 --- a/cmd/crates/soroban-test/tests/it/integration/wrap.rs +++ b/cmd/crates/soroban-test/tests/it/integration/wrap.rs @@ -1,4 +1,4 @@ -use soroban_cli::utils::contract_id_hash_from_asset; +use soroban_cli::{tx::builder, utils::contract_id_hash_from_asset}; use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; #[tokio::test] @@ -23,7 +23,7 @@ async fn burn() { .assert() .success(); // wrap_cmd(&asset).run().await.unwrap(); - let asset = soroban_cli::utils::parsing::parse_asset(&asset).unwrap(); + let asset: builder::Asset = asset.parse().unwrap(); let hash = contract_id_hash_from_asset(&asset, &network_passphrase); let id = stellar_strkey::Contract(hash.0).to_string(); println!("{id}, {address}"); diff --git a/cmd/crates/soroban-test/tests/it/util.rs b/cmd/crates/soroban-test/tests/it/util.rs index f424ea1ae..a74428666 100644 --- a/cmd/crates/soroban-test/tests/it/util.rs +++ b/cmd/crates/soroban-test/tests/it/util.rs @@ -51,7 +51,8 @@ pub async fn invoke_custom( arg: &str, wasm: &Path, ) -> Result { - let mut i: contract::invoke::Cmd = sandbox.cmd_with_config(&["--id", id, "--", func, arg]); + let mut i: contract::invoke::Cmd = + sandbox.cmd_with_config(&["--id", id, "--", func, arg], None); i.wasm = Some(wasm.to_path_buf()); sandbox .run_cmd_with(i, TEST_ACCOUNT) diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 75b562bb6..5f4cc7c5a 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -123,6 +123,7 @@ humantime = "2.1.0" phf = { version = "0.11.2", features = ["macros"] } semver = "1.0.0" glob = "0.3.1" +fqdn = "0.3.12" open = "5.3.0" url = "2.5.2" diff --git a/cmd/soroban-cli/src/commands/contract/alias/add.rs b/cmd/soroban-cli/src/commands/contract/alias/add.rs index d5c304452..5a7a17ddb 100644 --- a/cmd/soroban-cli/src/commands/contract/alias/add.rs +++ b/cmd/soroban-cli/src/commands/contract/alias/add.rs @@ -73,7 +73,7 @@ impl Cmd { self.config_locator.save_contract_id( &network.network_passphrase, - &self.contract_id.to_string(), + &self.contract_id, alias, )?; diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index da7b6b805..d2fcb62c4 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -4,7 +4,7 @@ use soroban_env_host::{ Asset, ContractDataDurability, ContractExecutable, ContractIdPreimage, CreateContractArgs, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, LedgerKey::ContractData, LedgerKeyContractData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, - ScAddress, ScVal, SequenceNumber, Transaction, TransactionExt, Uint256, VecM, WriteXdr, + ScAddress, ScVal, SequenceNumber, Transaction, TransactionExt, VecM, WriteXdr, }, HostError, }; @@ -19,7 +19,8 @@ use crate::{ }, config::{self, data, network}, rpc::{Client, Error as SorobanRpcError}, - utils::{contract_id_hash_from_asset, parsing::parse_asset}, + tx::builder, + utils::contract_id_hash_from_asset, }; #[derive(thiserror::Error, Debug)] @@ -39,11 +40,11 @@ pub enum Error { #[error(transparent)] Config(#[from] config::Error), #[error(transparent)] - ParseAssetError(#[from] crate::utils::parsing::Error), - #[error(transparent)] Data(#[from] data::Error), #[error(transparent)] Network(#[from] network::Error), + #[error(transparent)] + Builder(#[from] builder::Error), } impl From for Error { @@ -57,7 +58,7 @@ impl From for Error { pub struct Cmd { /// ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" #[arg(long)] - pub asset: String, + pub asset: builder::Asset, #[command(flatten)] pub config: config::Args, @@ -90,29 +91,29 @@ impl NetworkRunnable for Cmd { ) -> Result { let config = config.unwrap_or(&self.config); // Parse asset - let asset = parse_asset(&self.asset)?; + let asset = &self.asset; let network = config.get_network()?; let client = Client::new(&network.rpc_url)?; client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.source_account()?; + let source_account = config.source_account()?; // Get the account sequence number - let public_strkey = key.to_string(); + let public_strkey = source_account.to_string(); // TODO: use symbols for the method names (both here and in serve) let account_details = client.get_account(&public_strkey).await?; let sequence: i64 = account_details.seq_num.into(); let network_passphrase = &network.network_passphrase; - let contract_id = contract_id_hash_from_asset(&asset, network_passphrase); + let contract_id = contract_id_hash_from_asset(asset, network_passphrase); let tx = build_wrap_token_tx( - &asset, + asset, &contract_id, sequence + 1, self.fee.fee, network_passphrase, - &key, + source_account, )?; if self.fee.build_only { return Ok(TxnResult::Txn(tx)); @@ -135,14 +136,14 @@ impl NetworkRunnable for Cmd { } fn build_wrap_token_tx( - asset: &Asset, - contract_id: &Hash, + asset: impl Into, + contract_id: &stellar_strkey::Contract, sequence: i64, fee: u32, _network_passphrase: &str, - key: &stellar_strkey::ed25519::PublicKey, + source_account: MuxedAccount, ) -> Result { - let contract = ScAddress::Contract(contract_id.clone()); + let contract = ScAddress::Contract(Hash(contract_id.0)); let mut read_write = vec![ ContractData(LedgerKeyContractData { contract: contract.clone(), @@ -157,7 +158,8 @@ fn build_wrap_token_tx( durability: ContractDataDurability::Persistent, }), ]; - if asset != &Asset::Native { + let asset = asset.into(); + if asset != Asset::Native { read_write.push(ContractData(LedgerKeyContractData { contract, key: ScVal::Vec(Some( @@ -171,7 +173,7 @@ fn build_wrap_token_tx( source_account: None, body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { host_function: HostFunction::CreateContract(CreateContractArgs { - contract_id_preimage: ContractIdPreimage::Asset(asset.clone()), + contract_id_preimage: ContractIdPreimage::Asset(asset), executable: ContractExecutable::StellarAsset, }), auth: VecM::default(), @@ -179,7 +181,7 @@ fn build_wrap_token_tx( }; Ok(Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.0)), + source_account, fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 84138d432..42b31dd7d 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -115,6 +115,8 @@ pub enum Error { InvalidAliasFormat { alias: String }, #[error(transparent)] Locator(#[from] locator::Error), + #[error("Only ed25519 accounts are allowed")] + OnlyEd25519AccountsAllowed, } impl Cmd { @@ -158,13 +160,13 @@ fn alias_validator(alias: &str) -> Result { #[async_trait::async_trait] impl NetworkRunnable for Cmd { type Error = Error; - type Result = TxnResult; + type Result = TxnResult; async fn run_against_rpc_server( &self, global_args: Option<&global::Args>, config: Option<&config::Args>, - ) -> Result, Error> { + ) -> Result, Error> { let print = Print::new(global_args.map_or(false, |a| a.quiet)); let config = config.unwrap_or(&self.config); let wasm_hash = if let Some(wasm) = &self.wasm { @@ -190,12 +192,14 @@ impl NetworkRunnable for Cmd { .to_string() }; - let wasm_hash = Hash(utils::contract_id_from_str(&wasm_hash).map_err(|e| { - Error::CannotParseWasmHash { - wasm_hash: wasm_hash.clone(), - error: e, - } - })?); + let wasm_hash = Hash( + utils::contract_id_from_str(&wasm_hash) + .map_err(|e| Error::CannotParseWasmHash { + wasm_hash: wasm_hash.clone(), + error: e, + })? + .0, + ); print.infoln(format!("Using wasm hash {wasm_hash}").as_str()); @@ -212,11 +216,13 @@ impl NetworkRunnable for Cmd { client .verify_network_passphrase(Some(&network.network_passphrase)) .await?; - let key = config.source_account()?; + let MuxedAccount::Ed25519(bytes) = config.source_account()? else { + return Err(Error::OnlyEd25519AccountsAllowed); + }; + let key = stellar_strkey::ed25519::PublicKey(bytes.into()); // Get the account sequence number - let public_strkey = key.to_string(); - let account_details = client.get_account(&public_strkey).await?; + let account_details = client.get_account(&key.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let (txn, contract_id) = build_create_contract_tx( wasm_hash, @@ -224,7 +230,7 @@ impl NetworkRunnable for Cmd { self.fee.fee, &network.network_passphrase, salt, - &key, + key, )?; if self.fee.build_only { @@ -254,8 +260,6 @@ impl NetworkRunnable for Cmd { data::write(get_txn_resp, &network.rpc_uri()?)?; } - let contract_id = stellar_strkey::Contract(contract_id.0).to_string(); - if let Some(url) = utils::explorer_url_for_contract(&network, &contract_id) { print.linkln(url); } @@ -272,8 +276,8 @@ fn build_create_contract_tx( fee: u32, network_passphrase: &str, salt: [u8; 32], - key: &stellar_strkey::ed25519::PublicKey, -) -> Result<(Transaction, Hash), Error> { + key: stellar_strkey::ed25519::PublicKey, +) -> Result<(Transaction, stellar_strkey::Contract), Error> { let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(key.0.into())); let contract_id_preimage = ContractIdPreimage::Address(ContractIdPreimageFromAddress { @@ -293,7 +297,7 @@ fn build_create_contract_tx( }), }; let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.0)), + source_account: MuxedAccount::Ed25519(key.0.into()), fee, seq_num: SequenceNumber(sequence), cond: Preconditions::None, @@ -302,7 +306,7 @@ fn build_create_contract_tx( ext: TransactionExt::V0, }; - Ok((tx, Hash(contract_id.into()))) + Ok((tx, contract_id)) } #[cfg(test)] @@ -321,7 +325,7 @@ mod tests { 1, "Public Global Stellar Network ; September 2015", [0u8; 32], - &stellar_strkey::ed25519::PublicKey( + stellar_strkey::ed25519::PublicKey( utils::parse_secret_key("SBFGFF27Y64ZUGFAIG5AMJGQODZZKV2YQKAVUUN4HNE24XZXD2OEUVUP") .unwrap() .verifying_key() diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index 6666fcce7..b06cacf3e 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -3,9 +3,9 @@ use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ Error as XdrError, ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, LedgerEntryChange, - LedgerEntryData, LedgerFootprint, Limits, Memo, MuxedAccount, Operation, OperationBody, - Preconditions, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, - TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, WriteXdr, + LedgerEntryData, LedgerFootprint, Limits, Memo, Operation, OperationBody, Preconditions, + SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, TransactionExt, + TransactionMeta, TransactionMetaV3, TtlEntry, WriteXdr, }; use crate::{ @@ -132,15 +132,15 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let keys = self.key.parse_keys(&config.locator, &network)?; let client = Client::new(&network.rpc_url)?; - let key = config.source_account()?; + let source_account = config.source_account()?; let extend_to = self.ledgers_to_extend(); // Get the account sequence number - let account_details = client.get_account(&key.to_string()).await?; + let account_details = client.get_account(&source_account.to_string()).await?; let sequence: i64 = account_details.seq_num.into(); let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.0)), + source_account, fee: self.fee.fee, seq_num: SequenceNumber(sequence + 1), cond: Preconditions::None, diff --git a/cmd/soroban-cli/src/commands/contract/id/asset.rs b/cmd/soroban-cli/src/commands/contract/id/asset.rs index e57385859..63b3017a1 100644 --- a/cmd/soroban-cli/src/commands/contract/id/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/id/asset.rs @@ -2,23 +2,21 @@ use clap::{arg, command, Parser}; use crate::config; +use crate::tx::builder; use crate::utils::contract_id_hash_from_asset; -use crate::utils::parsing::parse_asset; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { /// ID of the Stellar classic asset to wrap, e.g. "USDC:G...5" #[arg(long)] - pub asset: String, + pub asset: builder::Asset, #[command(flatten)] pub config: config::Args, } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - ParseError(#[from] crate::utils::parsing::Error), #[error(transparent)] ConfigError(#[from] config::Error), #[error(transparent)] @@ -31,9 +29,8 @@ impl Cmd { } pub fn contract_address(&self) -> Result { - let asset = parse_asset(&self.asset)?; let network = self.config.get_network()?; - let contract_id = contract_id_hash_from_asset(&asset, &network.network_passphrase); + let contract_id = contract_id_hash_from_asset(&self.asset, &network.network_passphrase); Ok(stellar_strkey::Contract(contract_id.0)) } } diff --git a/cmd/soroban-cli/src/commands/contract/id/wasm.rs b/cmd/soroban-cli/src/commands/contract/id/wasm.rs index 14824b145..c020e94b2 100644 --- a/cmd/soroban-cli/src/commands/contract/id/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/id/wasm.rs @@ -19,14 +19,14 @@ pub struct Cmd { } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - ParseError(#[from] crate::utils::parsing::Error), #[error(transparent)] ConfigError(#[from] config::Error), #[error(transparent)] Xdr(#[from] xdr::Error), #[error("cannot parse salt {0}")] CannotParseSalt(String), + #[error("only Ed25519 accounts are allowed")] + OnlyEd25519AccountsAllowed, } impl Cmd { pub fn run(&self) -> Result<(), Error> { @@ -34,20 +34,25 @@ impl Cmd { .map_err(|_| Error::CannotParseSalt(self.salt.clone()))? .try_into() .map_err(|_| Error::CannotParseSalt(self.salt.clone()))?; - let contract_id_preimage = - contract_preimage(&self.config.key_pair()?.verifying_key(), salt); + let source_account = match self.config.source_account()? { + xdr::MuxedAccount::Ed25519(uint256) => stellar_strkey::ed25519::PublicKey(uint256.0), + xdr::MuxedAccount::MuxedEd25519(_) => return Err(Error::OnlyEd25519AccountsAllowed), + }; + let contract_id_preimage = contract_preimage(&source_account, salt); let contract_id = get_contract_id( contract_id_preimage.clone(), &self.config.get_network()?.network_passphrase, )?; - let strkey_contract_id = stellar_strkey::Contract(contract_id.0).to_string(); - println!("{strkey_contract_id}"); + println!("{contract_id}"); Ok(()) } } -pub fn contract_preimage(key: &ed25519_dalek::VerifyingKey, salt: [u8; 32]) -> ContractIdPreimage { - let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(key.to_bytes().into())); +pub fn contract_preimage( + key: &stellar_strkey::ed25519::PublicKey, + salt: [u8; 32], +) -> ContractIdPreimage { + let source_account = AccountId(PublicKey::PublicKeyTypeEd25519(key.0.into())); ContractIdPreimage::Address(ContractIdPreimageFromAddress { address: ScAddress::Account(source_account), salt: Uint256(salt), @@ -57,12 +62,14 @@ pub fn contract_preimage(key: &ed25519_dalek::VerifyingKey, salt: [u8; 32]) -> C pub fn get_contract_id( contract_id_preimage: ContractIdPreimage, network_passphrase: &str, -) -> Result { +) -> Result { let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId { network_id, contract_id_preimage, }); let preimage_xdr = preimage.to_xdr(Limits::none())?; - Ok(Hash(Sha256::digest(preimage_xdr).into())) + Ok(stellar_strkey::Contract( + Sha256::digest(preimage_xdr).into(), + )) } diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 5a1842b7b..9e8f6b098 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -5,9 +5,8 @@ use std::num::ParseIntError; use clap::{command, Parser}; use soroban_env_host::xdr::{ self, ContractCodeEntryExt, Error as XdrError, Hash, HostFunction, InvokeHostFunctionOp, - LedgerEntryData, Limits, Memo, MuxedAccount, Operation, OperationBody, Preconditions, ReadXdr, - ScMetaEntry, ScMetaV0, SequenceNumber, Transaction, TransactionExt, TransactionResult, - TransactionResultResult, Uint256, VecM, WriteXdr, + LedgerEntryData, Limits, OperationBody, ReadXdr, ScMetaEntry, ScMetaV0, Transaction, + TransactionResult, TransactionResultResult, VecM, WriteXdr, }; use super::restore; @@ -17,6 +16,7 @@ use crate::config::{self, data, network}; use crate::key; use crate::print::Print; use crate::rpc::{self, Client}; +use crate::tx::builder::{self, TxExt}; use crate::{utils, wasm}; const CONTRACT_META_SDK_KEY: &str = "rssdkver"; @@ -70,6 +70,8 @@ pub enum Error { Network(#[from] network::Error), #[error(transparent)] Data(#[from] data::Error), + #[error(transparent)] + Builder(#[from] builder::Error), } impl Cmd { @@ -253,27 +255,18 @@ pub(crate) fn build_install_contract_code_tx( source_code: &[u8], sequence: i64, fee: u32, - key: &stellar_strkey::ed25519::PublicKey, -) -> Result<(Transaction, Hash), XdrError> { + source: &xdr::MuxedAccount, +) -> Result<(Transaction, Hash), Error> { let hash = utils::contract_hash(source_code)?; - let op = Operation { - source_account: Some(MuxedAccount::Ed25519(Uint256(key.0))), + let op = xdr::Operation { + source_account: None, body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { host_function: HostFunction::UploadContractWasm(source_code.try_into()?), auth: VecM::default(), }), }; - - let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.0)), - fee, - seq_num: SequenceNumber(sequence), - cond: Preconditions::None, - memo: Memo::None, - operations: vec![op].try_into()?, - ext: TransactionExt::V0, - }; + let tx = Transaction::new_tx(source.clone(), fee, sequence, op); Ok((tx, hash)) } @@ -294,6 +287,9 @@ mod tests { .verifying_key() .as_bytes(), ) + .unwrap() + .to_string() + .parse() .unwrap(), ); diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index cf6a33248..05fa90695 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -3,9 +3,9 @@ use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ Error as XdrError, ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, - LedgerFootprint, Limits, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, - Preconditions, RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, - Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, Uint256, WriteXdr, + LedgerFootprint, Limits, Memo, Operation, OperationBody, OperationMeta, Preconditions, + RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, + TransactionExt, TransactionMeta, TransactionMetaV3, TtlEntry, WriteXdr, }; use stellar_strkey::DecodeError; @@ -135,15 +135,15 @@ impl NetworkRunnable for Cmd { tracing::trace!(?network); let entry_keys = self.key.parse_keys(&config.locator, &network)?; let client = Client::new(&network.rpc_url)?; - let key = config.source_account()?; + let source_account = config.source_account()?; // Get the account sequence number - let public_strkey = key.to_string(); + let public_strkey = source_account.to_string(); let account_details = client.get_account(&public_strkey).await?; let sequence: i64 = account_details.seq_num.into(); let tx = Transaction { - source_account: MuxedAccount::Ed25519(Uint256(key.0)), + source_account, fee: self.fee.fee, seq_num: SequenceNumber(sequence + 1), cond: Preconditions::None, diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index ee0f5f520..8a587e243 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -29,7 +29,8 @@ use crate::{ commands::{config::data, global, HEADING_RPC}, config::{self, locator, network::passphrase}, print, - utils::{get_name_from_stellar_asset_contract_storage, parsing::parse_asset}, + tx::builder, + utils::get_name_from_stellar_asset_contract_storage, }; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)] @@ -131,6 +132,8 @@ pub enum Error { ArchiveUrlNotConfigured, #[error("parsing asset name: {0}")] ParseAssetName(String), + #[error(transparent)] + Asset(#[from] builder::asset::Error), } /// Checkpoint frequency is usually 64 ledgers, but in local test nets it'll @@ -310,16 +313,11 @@ impl Cmd { if let Some(name) = get_name_from_stellar_asset_contract_storage(storage) { - let asset = parse_asset(&name) - .map_err(|_| Error::ParseAssetName(name))?; - if let Some(issuer) = match &asset { + let asset: builder::Asset = name.parse()?; + if let Some(issuer) = match asset.into() { Asset::Native => None, - Asset::CreditAlphanum4(a4) => { - Some(a4.issuer.clone()) - } - Asset::CreditAlphanum12(a12) => { - Some(a12.issuer.clone()) - } + Asset::CreditAlphanum4(a4) => Some(a4.issuer), + Asset::CreditAlphanum12(a12) => Some(a12.issuer), } { print.infoln(format!( "Adding asset issuer {issuer} to search" diff --git a/cmd/soroban-cli/src/commands/tx/args.rs b/cmd/soroban-cli/src/commands/tx/args.rs new file mode 100644 index 000000000..7e032fd53 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/args.rs @@ -0,0 +1,107 @@ +use crate::{ + commands::{global, txn_result::TxnEnvelopeResult}, + config::{self, data, network, secret}, + fee, + rpc::{self, Client, GetTransactionResponse}, + tx::builder::{self, TxExt}, + xdr::{self, Limits, WriteXdr}, +}; + +#[derive(Debug, clap::Args, Clone)] +#[group(skip)] +pub struct Args { + #[clap(flatten)] + pub fee: fee::Args, + #[clap(flatten)] + pub config: config::Args, +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Rpc(#[from] rpc::Error), + #[error(transparent)] + Config(#[from] config::Error), + #[error(transparent)] + Network(#[from] network::Error), + #[error(transparent)] + Secret(#[from] secret::Error), + #[error(transparent)] + Tx(#[from] builder::Error), + #[error(transparent)] + Data(#[from] data::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +impl Args { + pub async fn tx(&self, body: impl Into) -> Result { + let source_account = self.source_account()?; + let seq_num = self + .config + .next_sequence_number(&source_account.to_string()) + .await?; + // Once we have a way to add operations this will be updated to allow for a different source account + let operation = xdr::Operation { + source_account: None, + body: body.into(), + }; + Ok(xdr::Transaction::new_tx( + source_account, + self.fee.fee, + seq_num, + operation, + )) + } + + pub fn client(&self) -> Result { + let network = self.config.get_network()?; + Ok(Client::new(&network.rpc_url)?) + } + + pub async fn handle( + &self, + op: impl Into, + global_args: &global::Args, + ) -> Result, Error> { + let tx = self.tx(op.into()).await?; + self.handle_tx(tx, global_args).await + } + pub async fn handle_and_print( + &self, + op: impl Into, + global_args: &global::Args, + ) -> Result<(), Error> { + let res = self.handle(op, global_args).await?; + if let TxnEnvelopeResult::TxnEnvelope(tx) = res { + println!("{}", tx.to_xdr_base64(Limits::none())?); + }; + Ok(()) + } + + pub async fn handle_tx( + &self, + tx: xdr::Transaction, + args: &global::Args, + ) -> Result, Error> { + let network = self.config.get_network()?; + let client = Client::new(&network.rpc_url)?; + if self.fee.build_only { + return Ok(TxnEnvelopeResult::TxnEnvelope(tx.into())); + } + + let txn_resp = client + .send_transaction_polling(&self.config.sign_with_local_key(tx).await?) + .await?; + + if !args.no_cache { + data::write(txn_resp.clone().try_into().unwrap(), &network.rpc_uri()?)?; + } + + Ok(TxnEnvelopeResult::Res(txn_resp)) + } + + pub fn source_account(&self) -> Result { + Ok(self.config.source_account()?) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/mod.rs b/cmd/soroban-cli/src/commands/tx/mod.rs index 587a75fda..c0390f92e 100644 --- a/cmd/soroban-cli/src/commands/tx/mod.rs +++ b/cmd/soroban-cli/src/commands/tx/mod.rs @@ -1,14 +1,16 @@ -use clap::Parser; - use super::global; +pub mod args; pub mod hash; +pub mod new; pub mod send; pub mod sign; pub mod simulate; pub mod xdr; -#[derive(Debug, Parser)] +pub use args::Args; + +#[derive(Debug, clap::Subcommand)] pub enum Cmd { /// Simulate a transaction envelope from stdin Simulate(simulate::Cmd), @@ -18,15 +20,20 @@ pub enum Cmd { Sign(sign::Cmd), /// Send a transaction envelope to the network Send(send::Cmd), + /// Create a new transaction + #[command(subcommand)] + New(new::Cmd), } #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] - Simulate(#[from] simulate::Error), #[error(transparent)] Hash(#[from] hash::Error), #[error(transparent)] + New(#[from] new::Error), + #[error(transparent)] + Simulate(#[from] simulate::Error), + #[error(transparent)] Sign(#[from] sign::Error), #[error(transparent)] Send(#[from] send::Error), @@ -37,6 +44,7 @@ impl Cmd { match self { Cmd::Simulate(cmd) => cmd.run(global_args).await?, Cmd::Hash(cmd) => cmd.run(global_args)?, + Cmd::New(cmd) => cmd.run(global_args).await?, Cmd::Sign(cmd) => cmd.run(global_args).await?, Cmd::Send(cmd) => cmd.run(global_args).await?, }; diff --git a/cmd/soroban-cli/src/commands/tx/new/account_merge.rs b/cmd/soroban-cli/src/commands/tx/new/account_merge.rs new file mode 100644 index 000000000..ce01f5e1f --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/account_merge.rs @@ -0,0 +1,19 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Muxed Account to merge with, e.g. `GBX...`, 'MBX...' + #[arg(long)] + pub account: xdr::MuxedAccount, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + xdr::OperationBody::AccountMerge(cmd.account.clone()) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs new file mode 100644 index 000000000..dfb521f23 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/bump_sequence.rs @@ -0,0 +1,21 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Sequence number to bump to + #[arg(long)] + pub bump_to: i64, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + xdr::OperationBody::BumpSequence(xdr::BumpSequenceOp { + bump_to: cmd.bump_to.into(), + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/change_trust.rs b/cmd/soroban-cli/src/commands/tx/new/change_trust.rs new file mode 100644 index 000000000..1ea4e737e --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/change_trust.rs @@ -0,0 +1,29 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, tx::builder, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + #[arg(long)] + pub line: builder::Asset, + /// Limit for the trust line, 0 to remove the trust line + #[arg(long, default_value = u64::MAX.to_string())] + pub limit: i64, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let line = match cmd.line.0.clone() { + xdr::Asset::CreditAlphanum4(asset) => xdr::ChangeTrustAsset::CreditAlphanum4(asset), + xdr::Asset::CreditAlphanum12(asset) => xdr::ChangeTrustAsset::CreditAlphanum12(asset), + xdr::Asset::Native => xdr::ChangeTrustAsset::Native, + }; + xdr::OperationBody::ChangeTrust(xdr::ChangeTrustOp { + line, + limit: cmd.limit, + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/create_account.rs b/cmd/soroban-cli/src/commands/tx/new/create_account.rs new file mode 100644 index 000000000..967c0cf43 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/create_account.rs @@ -0,0 +1,25 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Account Id to create, e.g. `GBX...` + #[arg(long)] + pub destination: xdr::AccountId, + /// Initial balance in stroops of the account, default 1 XLM + #[arg(long, default_value = "10_000_000")] + pub starting_balance: i64, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + xdr::OperationBody::CreateAccount(xdr::CreateAccountOp { + destination: cmd.destination.clone(), + starting_balance: cmd.starting_balance, + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/manage_data.rs b/cmd/soroban-cli/src/commands/tx/new/manage_data.rs new file mode 100644 index 000000000..e0a029f02 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/manage_data.rs @@ -0,0 +1,29 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Line to change, either 4 or 12 alphanumeric characters, or "native" if not specified + #[arg(long)] + pub data_name: xdr::StringM<64>, + /// Up to 64 bytes long hex string + /// If not present then the existing Name will be deleted. + /// If present then this value will be set in the `DataEntry`. + #[arg(long)] + pub data_value: Option>, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let data_value = cmd.data_value.clone().map(Into::into); + let data_name = cmd.data_name.clone().into(); + xdr::OperationBody::ManageData(xdr::ManageDataOp { + data_name, + data_value, + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/mod.rs b/cmd/soroban-cli/src/commands/tx/new/mod.rs new file mode 100644 index 000000000..e5923f4ec --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/mod.rs @@ -0,0 +1,69 @@ +use clap::Parser; + +use super::global; + +mod account_merge; +mod bump_sequence; +mod change_trust; +mod create_account; +mod manage_data; +mod payment; +mod set_options; +mod set_trustline_flags; + +#[derive(Debug, Parser)] +#[allow(clippy::doc_markdown)] +pub enum Cmd { + /// Transfers the XLM balance of an account to another account and removes the source account from the ledger + AccountMerge(account_merge::Cmd), + /// Bumps forward the sequence number of the source account to the given sequence number, invalidating any transaction with a smaller sequence number + BumpSequence(bump_sequence::Cmd), + /// Creates, updates, or deletes a trustline + /// Learn more about trustlines + /// https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines + ChangeTrust(change_trust::Cmd), + /// Creates and funds a new account with the specified starting balance + CreateAccount(create_account::Cmd), + /// Sets, modifies, or deletes a data entry (name/value pair) that is attached to an account + /// Learn more about entries and subentries: + /// https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#subentries + ManageData(manage_data::Cmd), + /// Sends an amount in a specific asset to a destination account + Payment(payment::Cmd), + /// Set option for an account such as flags, inflation destination, signers, home domain, and master key weight + /// Learn more about flags: + /// https://developers.stellar.org/docs/learn/glossary#flags + /// Learn more about the home domain: + /// https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0001.md + /// Learn more about signers operations and key weight: + /// https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig + SetOptions(set_options::Cmd), + /// Allows issuing account to configure authorization and trustline flags to an asset + /// The Asset parameter is of the `TrustLineAsset` type. If you are modifying a trustline to a regular asset (i.e. one in a Code:Issuer format), this is equivalent to the Asset type. + /// If you are modifying a trustline to a pool share, however, this is composed of the liquidity pool's unique ID. + /// Learn more about flags: + /// https://developers.stellar.org/docs/learn/glossary#flags + SetTrustlineFlags(set_trustline_flags::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Tx(#[from] super::args::Error), +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + match self { + Cmd::AccountMerge(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::BumpSequence(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::ChangeTrust(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::CreateAccount(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::ManageData(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::Payment(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::SetOptions(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + Cmd::SetTrustlineFlags(cmd) => cmd.tx.handle_and_print(cmd, global_args).await, + }?; + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/payment.rs b/cmd/soroban-cli/src/commands/tx/new/payment.rs new file mode 100644 index 000000000..c626d9ca8 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/payment.rs @@ -0,0 +1,29 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, tx::builder, xdr}; + +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Account to send to, e.g. `GBX...` + #[arg(long)] + pub destination: xdr::MuxedAccount, + /// Asset to send, default native, e.i. XLM + #[arg(long, default_value = "native")] + pub asset: builder::Asset, + /// Amount of the aforementioned asset to send. + #[arg(long)] + pub amount: i64, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + xdr::OperationBody::Payment(xdr::PaymentOp { + destination: cmd.destination.clone(), + asset: cmd.asset.clone().into(), + amount: cmd.amount, + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/set_options.rs b/cmd/soroban-cli/src/commands/tx/new/set_options.rs new file mode 100644 index 000000000..3410b69e8 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/set_options.rs @@ -0,0 +1,123 @@ +use clap::{command, Parser}; + +use crate::{commands::tx, xdr}; + +#[derive(Parser, Debug, Clone)] +#[allow(clippy::struct_excessive_bools, clippy::doc_markdown)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + #[arg(long)] + /// Account of the inflation destination. + pub inflation_dest: Option, + #[arg(long)] + /// A number from 0-255 (inclusive) representing the weight of the master key. If the weight of the master key is updated to 0, it is effectively disabled. + pub master_weight: Option, + #[arg(long)] + /// A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a low threshold. + /// https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig + pub low_threshold: Option, + #[arg(long)] + /// A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a medium threshold. + /// https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig + pub med_threshold: Option, + #[arg(long)] + /// A number from 0-255 (inclusive) representing the threshold this account sets on all operations it performs that have a high threshold. + /// https://developers.stellar.org/docs/learn/encyclopedia/security/signatures-multisig#multisig + pub high_threshold: Option, + #[arg(long)] + /// Sets the home domain of an account. See https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/federation. + pub home_domain: Option>, + #[arg(long, requires = "signer_weight")] + /// Add, update, or remove a signer from an account. + pub signer: Option, + #[arg(long = "signer-weight", requires = "signer")] + /// Signer weight is a number from 0-255 (inclusive). The signer is deleted if the weight is 0. + pub signer_weight: Option, + #[arg(long, conflicts_with = "clear_required")] + /// When enabled, an issuer must approve an account before that account can hold its asset. + /// https://developers.stellar.org/docs/tokens/control-asset-access#authorization-required-0x1 + pub set_required: bool, + #[arg(long, conflicts_with = "clear_revocable")] + /// When enabled, an issuer can revoke an existing trustline’s authorization, thereby freezing the asset held by an account. + /// https://developers.stellar.org/docs/tokens/control-asset-access#authorization-revocable-0x2 + pub set_revocable: bool, + #[arg(long, conflicts_with = "clear_clawback_enabled")] + /// Enables the issuing account to take back (burning) all of the asset. + /// https://developers.stellar.org/docs/tokens/control-asset-access#clawback-enabled-0x8 + pub set_clawback_enabled: bool, + #[arg(long, conflicts_with = "clear_immutable")] + /// With this setting, none of the other authorization flags (`AUTH_REQUIRED_FLAG`, `AUTH_REVOCABLE_FLAG`) can be set, and the issuing account can’t be merged. + /// https://developers.stellar.org/docs/tokens/control-asset-access#authorization-immutable-0x4 + pub set_immutable: bool, + #[arg(long)] + pub clear_required: bool, + #[arg(long)] + pub clear_revocable: bool, + #[arg(long)] + pub clear_immutable: bool, + #[arg(long)] + pub clear_clawback_enabled: bool, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let mut set_flags = None; + let mut set_flag = |flag: xdr::AccountFlags| { + *set_flags.get_or_insert(0) |= flag as u32; + }; + + if cmd.set_required { + set_flag(xdr::AccountFlags::RequiredFlag); + }; + if cmd.set_revocable { + set_flag(xdr::AccountFlags::RevocableFlag); + }; + if cmd.set_immutable { + set_flag(xdr::AccountFlags::ImmutableFlag); + }; + if cmd.set_clawback_enabled { + set_flag(xdr::AccountFlags::ClawbackEnabledFlag); + }; + + let mut clear_flags = None; + let mut clear_flag = |flag: xdr::AccountFlags| { + *clear_flags.get_or_insert(0) |= flag as u32; + }; + if cmd.clear_required { + clear_flag(xdr::AccountFlags::RequiredFlag); + }; + if cmd.clear_revocable { + clear_flag(xdr::AccountFlags::RevocableFlag); + }; + if cmd.clear_immutable { + clear_flag(xdr::AccountFlags::ImmutableFlag); + }; + if cmd.clear_clawback_enabled { + clear_flag(xdr::AccountFlags::ClawbackEnabledFlag); + }; + + let signer = if let (Some(key), Some(signer_weight)) = + (cmd.signer.clone(), cmd.signer_weight.as_ref()) + { + Some(xdr::Signer { + key, + weight: u32::from(*signer_weight), + }) + } else { + None + }; + xdr::OperationBody::SetOptions(xdr::SetOptionsOp { + inflation_dest: cmd.inflation_dest.clone().map(Into::into), + clear_flags, + set_flags, + master_weight: cmd.master_weight.map(Into::into), + low_threshold: cmd.low_threshold.map(Into::into), + med_threshold: cmd.med_threshold.map(Into::into), + high_threshold: cmd.high_threshold.map(Into::into), + home_domain: cmd.home_domain.clone().map(Into::into), + signer, + }) + } +} diff --git a/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs b/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs new file mode 100644 index 000000000..2955fe5b0 --- /dev/null +++ b/cmd/soroban-cli/src/commands/tx/new/set_trustline_flags.rs @@ -0,0 +1,71 @@ +use clap::{command, Parser}; + +use soroban_sdk::xdr::{self}; + +use crate::{commands::tx, tx::builder}; + +#[allow(clippy::struct_excessive_bools, clippy::doc_markdown)] +#[derive(Parser, Debug, Clone)] +#[group(skip)] +pub struct Cmd { + #[command(flatten)] + pub tx: tx::Args, + /// Account to set trustline flags for + #[arg(long)] + pub trustor: xdr::AccountId, + /// Asset to set trustline flags for + #[arg(long)] + pub asset: builder::Asset, + #[arg(long, conflicts_with = "clear_authorize")] + /// Signifies complete authorization allowing an account to transact freely with the asset to make and receive payments and place orders. + pub set_authorize: bool, + #[arg(long, conflicts_with = "clear_authorize_to_maintain_liabilities")] + /// Denotes limited authorization that allows an account to maintain current orders but not to otherwise transact with the asset. + pub set_authorize_to_maintain_liabilities: bool, + #[arg(long, conflicts_with = "clear_trustline_clawback_enabled")] + /// Enables the issuing account to take back (burning) all of the asset. See our section on Clawbacks: + /// https://developers.stellar.org/docs/learn/encyclopedia/transactions-specialized/clawbacks + pub set_trustline_clawback_enabled: bool, + #[arg(long)] + pub clear_authorize: bool, + #[arg(long)] + pub clear_authorize_to_maintain_liabilities: bool, + #[arg(long)] + pub clear_trustline_clawback_enabled: bool, +} + +impl From<&Cmd> for xdr::OperationBody { + fn from(cmd: &Cmd) -> Self { + let mut set_flags = 0; + let mut set_flag = |flag: xdr::TrustLineFlags| set_flags |= flag as u32; + + if cmd.set_authorize { + set_flag(xdr::TrustLineFlags::AuthorizedFlag); + }; + if cmd.set_authorize_to_maintain_liabilities { + set_flag(xdr::TrustLineFlags::AuthorizedToMaintainLiabilitiesFlag); + }; + if cmd.set_trustline_clawback_enabled { + set_flag(xdr::TrustLineFlags::TrustlineClawbackEnabledFlag); + }; + + let mut clear_flags = 0; + let mut clear_flag = |flag: xdr::TrustLineFlags| clear_flags |= flag as u32; + if cmd.clear_authorize { + clear_flag(xdr::TrustLineFlags::AuthorizedFlag); + }; + if cmd.clear_authorize_to_maintain_liabilities { + clear_flag(xdr::TrustLineFlags::AuthorizedToMaintainLiabilitiesFlag); + }; + if cmd.clear_trustline_clawback_enabled { + clear_flag(xdr::TrustLineFlags::TrustlineClawbackEnabledFlag); + }; + + xdr::OperationBody::SetTrustLineFlags(xdr::SetTrustLineFlagsOp { + trustor: cmd.trustor.clone(), + asset: cmd.asset.clone().into(), + clear_flags, + set_flags, + }) + } +} diff --git a/cmd/soroban-cli/src/config/address.rs b/cmd/soroban-cli/src/config/address.rs new file mode 100644 index 000000000..066bc8d91 --- /dev/null +++ b/cmd/soroban-cli/src/config/address.rs @@ -0,0 +1,63 @@ +use std::str::FromStr; + +use crate::xdr; + +use super::{locator, secret}; + +/// Address can be either a public key or eventually an alias of a address. +#[derive(Clone, Debug)] +pub enum Address { + MuxedAccount(xdr::MuxedAccount), + AliasOrSecret(String), +} + +impl Default for Address { + fn default() -> Self { + Address::AliasOrSecret(String::default()) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + #[error(transparent)] + Secret(#[from] secret::Error), + #[error("Address cannot be used to sign {0}")] + CannotSign(xdr::MuxedAccount), +} + +impl FromStr for Address { + type Err = Error; + + fn from_str(value: &str) -> Result { + Ok(xdr::MuxedAccount::from_str(value).map_or_else( + |_| Address::AliasOrSecret(value.to_string()), + Address::MuxedAccount, + )) + } +} + +impl Address { + pub fn resolve_muxed_account( + &self, + locator: &locator::Args, + hd_path: Option, + ) -> Result { + match self { + Address::MuxedAccount(muxed_account) => Ok(muxed_account.clone()), + Address::AliasOrSecret(alias) => alias.parse().or_else(|_| { + Ok(xdr::MuxedAccount::Ed25519( + locator.read_identity(alias)?.public_key(hd_path)?.0.into(), + )) + }), + } + } + + pub fn resolve_secret(&self, locator: &locator::Args) -> Result { + match &self { + Address::MuxedAccount(muxed_account) => Err(Error::CannotSign(muxed_account.clone())), + Address::AliasOrSecret(alias) => Ok(locator.read_identity(alias)?), + } + } +} diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index bc167c977..2fad2bb62 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -214,7 +214,9 @@ impl Args { } pub fn read_identity(&self, name: &str) -> Result { - KeyType::Identity.read_with_global(name, &self.local_config()?) + Ok(KeyType::Identity + .read_with_global(name, &self.local_config()?) + .or_else(|_| name.parse())?) } pub fn key(&self, key_or_name: &str) -> Result { @@ -266,7 +268,7 @@ impl Args { pub fn save_contract_id( &self, network_passphrase: &str, - contract_id: &str, + contract_id: &stellar_strkey::Contract, alias: &str, ) -> Result<(), Error> { let path = self.alias_path(alias)?; @@ -284,7 +286,7 @@ impl Args { .open(path)?; data.ids - .insert(network_passphrase.into(), contract_id.into()); + .insert(network_passphrase.into(), contract_id.to_string()); let content = serde_json::to_string(&data)?; diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index e6cf8fa26..5b64a2697 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -1,5 +1,4 @@ -use std::path::PathBuf; - +use address::Address; use clap::{arg, command}; use serde::{Deserialize, Serialize}; @@ -8,12 +7,13 @@ use soroban_rpc::Client; use crate::{ print::Print, signer::{self, LocalKey, Signer, SignerKind}, - xdr::{Transaction, TransactionEnvelope}, + xdr::{self, SequenceNumber, Transaction, TransactionEnvelope}, Pwd, }; -use self::{network::Network, secret::Secret}; +use network::Network; +pub mod address; pub mod alias; pub mod data; pub mod locator; @@ -36,6 +36,8 @@ pub enum Error { Signer(#[from] signer::Error), #[error(transparent)] StellarStrkey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] + Address(#[from] address::Error), } #[derive(Debug, clap::Args, Clone, Default)] @@ -47,10 +49,11 @@ pub struct Args { #[arg(long, visible_alias = "source", env = "STELLAR_ACCOUNT")] /// Account that where transaction originates from. Alias `source`. /// Can be an identity (--source alice), a public key (--source GDKW...), - /// a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). + /// a muxed account (--source MDA…), a secret key (--source SC36…), + /// or a seed phrase (--source "kite urban…"). /// If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to /// sign the final transaction. In that case, trying to sign with public key will fail. - pub source_account: String, + pub source_account: Address, #[arg(long)] /// If using a seed phrase, which hierarchical deterministic path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` @@ -62,20 +65,14 @@ pub struct Args { impl Args { // TODO: Replace PublicKey with MuxedAccount once https://github.com/stellar/rs-stellar-xdr/pull/396 is merged. - pub fn source_account(&self) -> Result { - if let Ok(secret) = self.account(&self.source_account) { - Ok(stellar_strkey::ed25519::PublicKey( - secret.key_pair(self.hd_path)?.verifying_key().to_bytes(), - )) - } else { - Ok(stellar_strkey::ed25519::PublicKey::from_string( - &self.source_account, - )?) - } + pub fn source_account(&self) -> Result { + Ok(self + .source_account + .resolve_muxed_account(&self.locator, self.hd_path)?) } pub fn key_pair(&self) -> Result { - let key = self.account(&self.source_account)?; + let key = &self.source_account.resolve_secret(&self.locator)?; Ok(key.key_pair(self.hd_path)?) } @@ -113,20 +110,14 @@ impl Args { )?) } - pub fn account(&self, account_str: &str) -> Result { - if let Ok(secret) = self.locator.read_identity(account_str) { - Ok(secret) - } else { - Ok(account_str.parse::()?) - } - } - pub fn get_network(&self) -> Result { Ok(self.network.get(&self.locator)?) } - pub fn config_dir(&self) -> Result { - Ok(self.locator.config_dir()?) + pub async fn next_sequence_number(&self, account_str: &str) -> Result { + let network = self.get_network()?; + let client = Client::new(&network.rpc_url)?; + Ok((client.get_account(account_str).await?.seq_num.0 + 1).into()) } } diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 2a6a591ca..f5ea21884 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -20,6 +20,7 @@ pub mod log; pub mod print; pub mod signer; pub mod toid; +pub mod tx; pub mod upgrade_check; pub mod utils; pub mod wasm; diff --git a/cmd/soroban-cli/src/tx.rs b/cmd/soroban-cli/src/tx.rs new file mode 100644 index 000000000..940b67305 --- /dev/null +++ b/cmd/soroban-cli/src/tx.rs @@ -0,0 +1,4 @@ +pub mod builder; + +/// 10,000,000 stroops in 1 XLM +pub const ONE_XLM: i64 = 10_000_000; diff --git a/cmd/soroban-cli/src/tx/builder.rs b/cmd/soroban-cli/src/tx/builder.rs new file mode 100644 index 000000000..ad22737ea --- /dev/null +++ b/cmd/soroban-cli/src/tx/builder.rs @@ -0,0 +1,11 @@ +pub mod asset; +pub mod transaction; + +pub use asset::Asset; +pub use transaction::TxExt; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Transaction contains too many operations")] + TooManyOperations, +} diff --git a/cmd/soroban-cli/src/tx/builder/asset.rs b/cmd/soroban-cli/src/tx/builder/asset.rs new file mode 100644 index 000000000..bba39804e --- /dev/null +++ b/cmd/soroban-cli/src/tx/builder/asset.rs @@ -0,0 +1,50 @@ +use std::str::FromStr; + +use crate::xdr::{self, AlphaNum12, AlphaNum4, AssetCode}; + +#[derive(Clone, Debug)] +pub struct Asset(pub xdr::Asset); + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("cannot parse asset: {0}, expected format: 'native' or 'code:issuer'")] + CannotParseAsset(String), + + #[error(transparent)] + Xdr(#[from] xdr::Error), +} + +impl FromStr for Asset { + type Err = Error; + + fn from_str(value: &str) -> Result { + if value == "native" { + return Ok(Asset(xdr::Asset::Native)); + } + let mut iter = value.splitn(2, ':'); + let (Some(code), Some(issuer), None) = (iter.next(), iter.next(), iter.next()) else { + return Err(Error::CannotParseAsset(value.to_string())); + }; + let issuer = issuer.parse()?; + Ok(Asset(match code.parse()? { + AssetCode::CreditAlphanum4(asset_code) => { + xdr::Asset::CreditAlphanum4(AlphaNum4 { asset_code, issuer }) + } + AssetCode::CreditAlphanum12(asset_code) => { + xdr::Asset::CreditAlphanum12(AlphaNum12 { asset_code, issuer }) + } + })) + } +} + +impl From for xdr::Asset { + fn from(builder: Asset) -> Self { + builder.0 + } +} + +impl From<&Asset> for xdr::Asset { + fn from(builder: &Asset) -> Self { + builder.clone().into() + } +} diff --git a/cmd/soroban-cli/src/tx/builder/transaction.rs b/cmd/soroban-cli/src/tx/builder/transaction.rs new file mode 100644 index 000000000..51232d5c5 --- /dev/null +++ b/cmd/soroban-cli/src/tx/builder/transaction.rs @@ -0,0 +1,53 @@ +use crate::xdr::{self, Memo, SequenceNumber, TransactionExt}; + +use super::Error; + +pub trait TxExt { + fn new_tx( + source: xdr::MuxedAccount, + fee: u32, + seq_num: impl Into, + operation: xdr::Operation, + ) -> xdr::Transaction; + + fn add_operation(self, operation: xdr::Operation) -> Result; + + fn add_memo(self, memo: Memo) -> xdr::Transaction; + + fn add_cond(self, cond: xdr::Preconditions) -> xdr::Transaction; +} + +impl TxExt for xdr::Transaction { + fn new_tx( + source_account: xdr::MuxedAccount, + fee: u32, + seq_num: impl Into, + operation: xdr::Operation, + ) -> xdr::Transaction { + xdr::Transaction { + source_account, + fee, + seq_num: seq_num.into(), + cond: soroban_env_host::xdr::Preconditions::None, + memo: Memo::None, + operations: [operation].try_into().unwrap(), + ext: TransactionExt::V0, + } + } + + fn add_operation(mut self, operation: xdr::Operation) -> Result { + let mut ops = self.operations.to_vec(); + ops.push(operation); + self.operations = ops.try_into().map_err(|_| Error::TooManyOperations)?; + Ok(self) + } + + fn add_memo(mut self, memo: Memo) -> Self { + self.memo = memo; + self + } + + fn add_cond(self, cond: xdr::Preconditions) -> xdr::Transaction { + xdr::Transaction { cond, ..self } + } +} diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index f5827f75b..8d0090042 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -3,8 +3,8 @@ use sha2::{Digest, Sha256}; use stellar_strkey::ed25519::PrivateKey; use soroban_env_host::xdr::{ - Asset, ContractIdPreimage, Error as XdrError, Hash, HashIdPreimage, HashIdPreimageContractId, - Limits, ScMap, ScMapEntry, ScVal, Transaction, TransactionSignaturePayload, + self, Asset, ContractIdPreimage, Hash, HashIdPreimage, HashIdPreimageContractId, Limits, ScMap, + ScMapEntry, ScVal, Transaction, TransactionSignaturePayload, TransactionSignaturePayloadTaggedTransaction, WriteXdr, }; @@ -15,14 +15,17 @@ use crate::config::network::Network; /// # Errors /// /// Might return an error -pub fn contract_hash(contract: &[u8]) -> Result { +pub fn contract_hash(contract: &[u8]) -> Result { Ok(Hash(Sha256::digest(contract).into())) } /// # Errors /// /// Might return an error -pub fn transaction_hash(tx: &Transaction, network_passphrase: &str) -> Result<[u8; 32], XdrError> { +pub fn transaction_hash( + tx: &Transaction, + network_passphrase: &str, +) -> Result<[u8; 32], xdr::Error> { let signature_payload = TransactionSignaturePayload { network_id: Hash(Sha256::digest(network_passphrase).into()), tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(tx.clone()), @@ -41,7 +44,10 @@ pub fn explorer_url_for_transaction(network: &Network, tx_hash: &str) -> Option< .map(|base_url| format!("{base_url}/tx/{tx_hash}")) } -pub fn explorer_url_for_contract(network: &Network, contract_id: &str) -> Option { +pub fn explorer_url_for_contract( + network: &Network, + contract_id: &stellar_strkey::Contract, +) -> Option { EXPLORERS .get(&network.network_passphrase) .map(|base_url| format!("{base_url}/contract/{contract_id}")) @@ -50,16 +56,20 @@ pub fn explorer_url_for_contract(network: &Network, contract_id: &str) -> Option /// # Errors /// /// Might return an error -pub fn contract_id_from_str(contract_id: &str) -> Result<[u8; 32], stellar_strkey::DecodeError> { +pub fn contract_id_from_str( + contract_id: &str, +) -> Result { Ok( if let Ok(strkey) = stellar_strkey::Contract::from_string(contract_id) { - strkey.0 + strkey } else { // strkey failed, try to parse it as a hex string, for backwards compatibility. - soroban_spec_tools::utils::padded_hex_from_str(contract_id, 32) - .map_err(|_| stellar_strkey::DecodeError::Invalid)? - .try_into() - .map_err(|_| stellar_strkey::DecodeError::Invalid)? + stellar_strkey::Contract( + soroban_spec_tools::utils::padded_hex_from_str(contract_id, 32) + .map_err(|_| stellar_strkey::DecodeError::Invalid)? + .try_into() + .map_err(|_| stellar_strkey::DecodeError::Invalid)?, + ) }, ) } @@ -112,16 +122,19 @@ pub fn is_hex_string(s: &str) -> bool { s.chars().all(|s| s.is_ascii_hexdigit()) } -pub fn contract_id_hash_from_asset(asset: &Asset, network_passphrase: &str) -> Hash { +pub fn contract_id_hash_from_asset( + asset: impl Into, + network_passphrase: &str, +) -> stellar_strkey::Contract { let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId { network_id, - contract_id_preimage: ContractIdPreimage::Asset(asset.clone()), + contract_id_preimage: ContractIdPreimage::Asset(asset.into()), }); let preimage_xdr = preimage .to_xdr(Limits::none()) .expect("HashIdPreimage should not fail encoding to xdr"); - Hash(Sha256::digest(preimage_xdr).into()) + stellar_strkey::Contract(Sha256::digest(preimage_xdr).into()) } pub fn get_name_from_stellar_asset_contract_storage(storage: &ScMap) -> Option { @@ -171,74 +184,6 @@ pub mod rpc { } } -pub mod parsing { - - use regex::Regex; - use soroban_env_host::xdr::{ - AccountId, AlphaNum12, AlphaNum4, Asset, AssetCode12, AssetCode4, PublicKey, - }; - - #[derive(thiserror::Error, Debug)] - pub enum Error { - #[error("invalid asset code: {asset}")] - InvalidAssetCode { asset: String }, - #[error("cannot parse account id: {account_id}")] - CannotParseAccountId { account_id: String }, - #[error("cannot parse asset: {asset}")] - CannotParseAsset { asset: String }, - #[error(transparent)] - Regex(#[from] regex::Error), - } - - pub fn parse_asset(str: &str) -> Result { - if str == "native" { - return Ok(Asset::Native); - } - let split: Vec<&str> = str.splitn(2, ':').collect(); - if split.len() != 2 { - return Err(Error::CannotParseAsset { - asset: str.to_string(), - }); - } - let code = split[0]; - let issuer = split[1]; - let re = Regex::new("^[[:alnum:]]{1,12}$")?; - if !re.is_match(code) { - return Err(Error::InvalidAssetCode { - asset: str.to_string(), - }); - } - if code.len() <= 4 { - let mut asset_code: [u8; 4] = [0; 4]; - for (i, b) in code.as_bytes().iter().enumerate() { - asset_code[i] = *b; - } - Ok(Asset::CreditAlphanum4(AlphaNum4 { - asset_code: AssetCode4(asset_code), - issuer: parse_account_id(issuer)?, - })) - } else { - let mut asset_code: [u8; 12] = [0; 12]; - for (i, b) in code.as_bytes().iter().enumerate() { - asset_code[i] = *b; - } - Ok(Asset::CreditAlphanum12(AlphaNum12 { - asset_code: AssetCode12(asset_code), - issuer: parse_account_id(issuer)?, - })) - } - } - - pub fn parse_account_id(str: &str) -> Result { - let pk_bytes = stellar_strkey::ed25519::PublicKey::from_string(str) - .map_err(|_| Error::CannotParseAccountId { - account_id: str.to_string(), - })? - .0; - Ok(AccountId(PublicKey::PublicKeyTypeEd25519(pk_bytes.into()))) - } -} - #[cfg(test)] mod tests { use super::*; @@ -248,7 +193,7 @@ mod tests { // strkey match contract_id_from_str("CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE") { Ok(contract_id) => assert_eq!( - contract_id, + contract_id.0, [ 0x36, 0x3e, 0xaa, 0x38, 0x67, 0x84, 0x1f, 0xba, 0xd0, 0xf4, 0xed, 0x88, 0xc7, 0x79, 0xe4, 0xfe, 0x66, 0xe5, 0x6a, 0x24, 0x70, 0xdc, 0x98, 0xc0, 0xec, 0x9c, From bb9c712004c3819506442c97c6872ec4f8c28a95 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Wed, 2 Oct 2024 16:14:34 -0400 Subject: [PATCH 3/8] fix: add test to check if default is used for durability (#1644) --- cmd/crates/soroban-test/tests/it/integration/util.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/crates/soroban-test/tests/it/integration/util.rs b/cmd/crates/soroban-test/tests/it/integration/util.rs index 22ef4fa82..6feae7860 100644 --- a/cmd/crates/soroban-test/tests/it/integration/util.rs +++ b/cmd/crates/soroban-test/tests/it/integration/util.rs @@ -95,14 +95,7 @@ pub async fn extend_contract(sandbox: &TestEnv, id: &str) { } pub async fn extend(sandbox: &TestEnv, id: &str, value: Option<&str>) { - let mut args = vec![ - "--id", - id, - "--durability", - "persistent", - "--ledgers-to-extend", - "100001", - ]; + let mut args = vec!["--id", id, "--ledgers-to-extend", "100001"]; if let Some(value) = value { args.push("--key"); args.push(value); From ddad905341042867b32f02cbe005bcedd5a06f78 Mon Sep 17 00:00:00 2001 From: Nando Vieira Date: Wed, 2 Oct 2024 17:12:44 -0700 Subject: [PATCH 4/8] Do not require source account when fetching an asset's contract id. (#1647) --- FULL_HELP_DOCS.md | 8 ++------ .../tests/it/integration/cookbook.rs | 2 -- .../src/commands/contract/id/asset.rs | 2 +- cmd/soroban-cli/src/config/mod.rs | 16 ++++++++++++++++ cookbook/deploy-stellar-asset-contract.mdx | 1 - cookbook/payments-and-assets.mdx | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/FULL_HELP_DOCS.md b/FULL_HELP_DOCS.md index be20ae24a..b8b03c580 100644 --- a/FULL_HELP_DOCS.md +++ b/FULL_HELP_DOCS.md @@ -111,7 +111,7 @@ Utilities to deploy a Stellar Asset Contract or get its id Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id asset` instead -**Usage:** `stellar contract asset id [OPTIONS] --asset --source-account ` +**Usage:** `stellar contract asset id [OPTIONS] --asset ` ###### **Options:** @@ -119,8 +119,6 @@ Get Id of builtin Soroban Asset Contract. Deprecated, use `stellar contract id a * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail -* `--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 ` — Location of config directory, default is "." @@ -444,7 +442,7 @@ Generate the contract id for a given contract or asset Deploy builtin Soroban Asset Contract -**Usage:** `stellar contract id asset [OPTIONS] --asset --source-account ` +**Usage:** `stellar contract id asset [OPTIONS] --asset ` ###### **Options:** @@ -452,8 +450,6 @@ Deploy builtin Soroban Asset Contract * `--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 -* `--source-account ` — Account that where transaction originates from. Alias `source`. Can be an identity (--source alice), a public key (--source GDKW...), a muxed account (--source MDA…), a secret key (--source SC36…), or a seed phrase (--source "kite urban…"). If `--build-only` or `--sim-only` flags were NOT provided, this key will also be used to sign the final transaction. In that case, trying to sign with public key will fail -* `--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 ` — Location of config directory, default is "." diff --git a/cmd/crates/soroban-test/tests/it/integration/cookbook.rs b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs index 4c0ad5a63..82b9c5f43 100644 --- a/cmd/crates/soroban-test/tests/it/integration/cookbook.rs +++ b/cmd/crates/soroban-test/tests/it/integration/cookbook.rs @@ -236,8 +236,6 @@ mod tests { .arg("asset") .arg("--asset") .arg("native") - .arg("--source-account") - .arg(source) .assert() .stdout_as_str(); let contract_id = deploy_hello(&sandbox).await; diff --git a/cmd/soroban-cli/src/commands/contract/id/asset.rs b/cmd/soroban-cli/src/commands/contract/id/asset.rs index 63b3017a1..cdf826015 100644 --- a/cmd/soroban-cli/src/commands/contract/id/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/id/asset.rs @@ -13,7 +13,7 @@ pub struct Cmd { pub asset: builder::Asset, #[command(flatten)] - pub config: config::Args, + pub config: config::ArgsLocatorAndNetwork, } #[derive(thiserror::Error, Debug)] pub enum Error { diff --git a/cmd/soroban-cli/src/config/mod.rs b/cmd/soroban-cli/src/config/mod.rs index 5b64a2697..12f571a50 100644 --- a/cmd/soroban-cli/src/config/mod.rs +++ b/cmd/soroban-cli/src/config/mod.rs @@ -129,3 +129,19 @@ impl Pwd for Args { #[derive(Default, Serialize, Deserialize)] pub struct Config {} + +#[derive(Debug, clap::Args, Clone, Default)] +#[group(skip)] +pub struct ArgsLocatorAndNetwork { + #[command(flatten)] + pub network: network::Args, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl ArgsLocatorAndNetwork { + pub fn get_network(&self) -> Result { + Ok(self.network.get(&self.locator)?) + } +} diff --git a/cookbook/deploy-stellar-asset-contract.mdx b/cookbook/deploy-stellar-asset-contract.mdx index 7233acc60..8fca51c48 100644 --- a/cookbook/deploy-stellar-asset-contract.mdx +++ b/cookbook/deploy-stellar-asset-contract.mdx @@ -42,7 +42,6 @@ For any asset, the contract address can be fetched with: ```bash stellar contract id asset \ - --source S... \ --network testnet \ --asset native ``` diff --git a/cookbook/payments-and-assets.mdx b/cookbook/payments-and-assets.mdx index 9849bf3d9..e3be8dafc 100644 --- a/cookbook/payments-and-assets.mdx +++ b/cookbook/payments-and-assets.mdx @@ -35,7 +35,7 @@ stellar keys fund bob 3. Obtain the stellar asset contract ID: ```bash -stellar contract id asset --asset native --source-account alice +stellar contract id asset --asset native ``` 4. Get Bob's public key: From 2cb77d27df85c645cf7d5fab14e3586875ef581a Mon Sep 17 00:00:00 2001 From: Jun Luo <4catcode@gmail.com> Date: Thu, 3 Oct 2024 18:58:19 +0800 Subject: [PATCH 5/8] Use reqwest, remove http, ureq and hyper (#1632) --- Cargo.lock | 87 ++++++---- Cargo.toml | 1 - cmd/soroban-cli/Cargo.toml | 23 ++- cmd/soroban-cli/src/cli.rs | 5 +- cmd/soroban-cli/src/commands/contract/init.rs | 4 +- cmd/soroban-cli/src/commands/network/mod.rs | 4 +- .../src/commands/snapshot/create.rs | 81 +++++----- cmd/soroban-cli/src/config/data.rs | 14 +- cmd/soroban-cli/src/config/network.rs | 149 +++++++++++++----- cmd/soroban-cli/src/upgrade_check.rs | 24 +-- cmd/soroban-cli/src/utils.rs | 35 ++++ 11 files changed, 275 insertions(+), 152 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21df05973..7ce514d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -779,6 +779,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2859,6 +2869,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -3528,6 +3539,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mockito" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b34bd91b9e5c5b06338d392463e1318d683cf82ec3d3af4014609be6e2108d" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "log", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3693,15 +3728,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-src" -version = "300.3.1+3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.103" @@ -3710,7 +3736,6 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -4308,6 +4333,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2 0.4.6", @@ -4336,10 +4362,12 @@ dependencies = [ "sync_wrapper 1.0.1", "tokio", "tokio-rustls 0.26.0", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots 0.26.3", "windows-registry", @@ -4521,7 +4549,6 @@ version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -5133,21 +5160,19 @@ dependencies = [ "heck 0.5.0", "hex", "home", - "http 0.2.12", "humantime", - "hyper 0.14.30", - "hyper-tls", "itertools 0.10.5", "jsonrpsee-core", "jsonrpsee-http-client", + "mockito", "num-bigint", "open", - "openssl", "pathdiff", "phf", "predicates 2.1.5", "rand", "regex", + "reqwest 0.12.7", "rpassword", "rust-embed", "semver", @@ -5183,7 +5208,6 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "ulid", - "ureq", "url", "walkdir", "wasm-opt", @@ -6341,24 +6365,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" -dependencies = [ - "base64 0.22.1", - "flate2", - "log", - "once_cell", - "rustls 0.23.12", - "rustls-pki-types", - "serde", - "serde_json", - "url", - "webpki-roots 0.26.3", -] - [[package]] name = "url" version = "2.5.2" @@ -6547,6 +6553,19 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmi_arena" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 4cf12b4d9..7282ff497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,6 @@ termcolor_output = "1.0.1" ed25519-dalek = ">= 2.1.1" # networking -http = "1.0.0" jsonrpsee-http-client = "0.20.1" jsonrpsee-core = "0.20.1" tokio = "1.28.1" diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 5f4cc7c5a..646e8a92e 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -74,14 +74,18 @@ wasmparser = { workspace = true } sha2 = { workspace = true } csv = "1.1.6" ed25519-dalek = { workspace = true } +reqwest = { version = "0.12.7", default-features = false, features = [ + "rustls-tls", + "http2", + "json", + "blocking", + "stream", +] } jsonrpsee-http-client = "0.20.1" jsonrpsee-core = "0.20.1" -hyper = "0.14.27" -hyper-tls = "0.5" -http = "0.2.9" regex = "1.6.0" wasm-opt = { version = "0.114.0", optional = true } -chrono = { version = "0.4.27", features = ["serde"]} +chrono = { version = "0.4.27", features = ["serde"] } rpassword = "7.2.0" dirs = "4.0.0" toml = "0.5.9" @@ -107,13 +111,12 @@ gix = { version = "0.58.0", default-features = false, features = [ "blocking-http-transport-reqwest-rust-tls", "worktree-mutation", ] } -ureq = { version = "2.9.1", features = ["json"] } -async-compression = { version = "0.4.12", features = [ "tokio", "gzip" ] } +async-compression = { version = "0.4.12", features = ["tokio", "gzip"] } tempfile = "3.8.1" toml_edit = "0.21.0" rust-embed = { version = "8.2.0", features = ["debug-embed"] } -bollard = { workspace=true } +bollard = { workspace = true } futures-util = "0.3.30" futures = "0.3.30" home = "0.5.9" @@ -127,15 +130,10 @@ fqdn = "0.3.12" open = "5.3.0" url = "2.5.2" -# For hyper-tls -[target.'cfg(unix)'.dependencies] -openssl = { version = "=0.10.55", features = ["vendored"] } - [build-dependencies] crate-git-revision = "0.0.4" serde.workspace = true thiserror.workspace = true -ureq = { version = "2.9.1", features = ["json"] } [dev-dependencies] @@ -143,3 +141,4 @@ assert_cmd = "2.0.4" assert_fs = "1.0.7" predicates = "2.1.5" walkdir = "2.5.0" +mockito = "1.5.0" diff --git a/cmd/soroban-cli/src/cli.rs b/cmd/soroban-cli/src/cli.rs index efed1b63b..5470562db 100644 --- a/cmd/soroban-cli/src/cli.rs +++ b/cmd/soroban-cli/src/cli.rs @@ -1,6 +1,5 @@ use clap::CommandFactory; use dotenvy::dotenv; -use std::thread; use tracing_subscriber::{fmt, EnvFilter}; use crate::upgrade_check::upgrade_check; @@ -75,8 +74,8 @@ pub async fn main() { // Spawn a thread to check if a new version exists. // It depends on logger, so we need to place it after // the code block that initializes the logger. - thread::spawn(move || { - upgrade_check(root.global_args.quiet); + tokio::spawn(async move { + upgrade_check(root.global_args.quiet).await; }); let printer = print::Print::new(root.global_args.quiet); diff --git a/cmd/soroban-cli/src/commands/contract/init.rs b/cmd/soroban-cli/src/commands/contract/init.rs index 18938d001..fd4cf483a 100644 --- a/cmd/soroban-cli/src/commands/contract/init.rs +++ b/cmd/soroban-cli/src/commands/contract/init.rs @@ -19,8 +19,8 @@ use std::{ sync::atomic::AtomicBool, }; use toml_edit::{Document, TomlError}; -use ureq::get; +use crate::utils::http; use crate::{commands::global, print}; const SOROBAN_EXAMPLES_URL: &str = "https://github.com/stellar/soroban-examples.git"; @@ -261,7 +261,7 @@ impl Runner { } fn check_internet_connection() -> bool { - if let Ok(_req) = get(GITHUB_URL).call() { + if let Ok(_req) = http::blocking_client().get(GITHUB_URL).send() { return true; } diff --git a/cmd/soroban-cli/src/commands/network/mod.rs b/cmd/soroban-cli/src/commands/network/mod.rs index 772d0cbe8..8dd61b394 100644 --- a/cmd/soroban-cli/src/commands/network/mod.rs +++ b/cmd/soroban-cli/src/commands/network/mod.rs @@ -67,11 +67,9 @@ pub enum Error { #[error("network arg or rpc url and network passphrase are required if using the network")] Network, #[error(transparent)] - Http(#[from] http::Error), - #[error(transparent)] Rpc(#[from] rpc::Error), #[error(transparent)] - Hyper(#[from] hyper::Error), + HttpClient(#[from] reqwest::Error), #[error("Failed to parse JSON from {0}, {1}")] FailedToParseJSON(String, serde_json::Error), #[error("Invalid URL {0}")] diff --git a/cmd/soroban-cli/src/commands/snapshot/create.rs b/cmd/soroban-cli/src/commands/snapshot/create.rs index 8a587e243..13bef9465 100644 --- a/cmd/soroban-cli/src/commands/snapshot/create.rs +++ b/cmd/soroban-cli/src/commands/snapshot/create.rs @@ -1,8 +1,7 @@ use async_compression::tokio::bufread::GzipDecoder; use bytesize::ByteSize; use clap::{arg, Parser, ValueEnum}; -use futures::{StreamExt, TryStreamExt}; -use http::Uri; +use futures::StreamExt; use humantime::format_duration; use itertools::{Either, Itertools}; use sha2::{Digest, Sha256}; @@ -24,7 +23,11 @@ use stellar_xdr::curr::{ ScVal, }; use tokio::fs::OpenOptions; +use tokio::io::BufReader; +use tokio_util::io::StreamReader; +use url::Url; +use crate::utils::http; use crate::{ commands::{config::data, global, HEADING_RPC}, config::{self, locator, network::passphrase}, @@ -85,7 +88,7 @@ pub struct Cmd { network: config::network::Args, /// Archive URL #[arg(long, help_heading = HEADING_RPC, env = "STELLAR_ARCHIVE_URL")] - archive_url: Option, + archive_url: Option, } #[derive(thiserror::Error, Debug)] @@ -93,19 +96,19 @@ pub enum Error { #[error("wasm hash invalid: {0}")] WasmHashInvalid(String), #[error("downloading history: {0}")] - DownloadingHistory(hyper::Error), + DownloadingHistory(reqwest::Error), #[error("downloading history: got status code {0}")] - DownloadingHistoryGotStatusCode(hyper::StatusCode), + DownloadingHistoryGotStatusCode(reqwest::StatusCode), #[error("json decoding history: {0}")] JsonDecodingHistory(serde_json::Error), #[error("opening cached bucket to read: {0}")] ReadOpeningCachedBucket(io::Error), #[error("parsing bucket url: {0}")] - ParsingBucketUrl(http::uri::InvalidUri), + ParsingBucketUrl(url::ParseError), #[error("getting bucket: {0}")] - GettingBucket(hyper::Error), + GettingBucket(reqwest::Error), #[error("getting bucket: got status code {0}")] - GettingBucketGotStatusCode(hyper::StatusCode), + GettingBucketGotStatusCode(reqwest::StatusCode), #[error("opening cached bucket to write: {0}")] WriteOpeningCachedBucket(io::Error), #[error("streaming bucket: {0}")] @@ -117,7 +120,7 @@ pub enum Error { #[error("getting bucket directory: {0}")] GetBucketDir(data::Error), #[error("reading history http stream: {0}")] - ReadHistoryHttpStream(hyper::Error), + ReadHistoryHttpStream(reqwest::Error), #[error("writing ledger snapshot: {0}")] WriteLedgerSnapshot(soroban_ledger_snapshot::Error), #[error(transparent)] @@ -362,7 +365,7 @@ impl Cmd { Ok(()) } - fn archive_url(&self) -> Result { + fn archive_url(&self) -> Result { // Return the configured archive URL, or if one is not configured, guess // at an appropriate archive URL given the network passphrase. self.archive_url @@ -380,7 +383,7 @@ impl Cmd { passphrase::LOCAL => Some("http://localhost:8000/archive"), _ => None, } - .map(|s| Uri::from_str(s).expect("archive url valid")) + .map(|s| Url::from_str(s).expect("archive url valid")) }) }) .ok_or(Error::ArchiveUrlNotConfigured) @@ -389,7 +392,7 @@ impl Cmd { async fn get_history( print: &print::Print, - archive_url: &Uri, + archive_url: &Url, ledger: Option, ) -> Result { let archive_url = archive_url.to_string(); @@ -403,14 +406,13 @@ async fn get_history( } else { format!("{archive_url}/.well-known/stellar-history.json") }; - let history_url = Uri::from_str(&history_url).unwrap(); + let history_url = Url::from_str(&history_url).unwrap(); print.globe(format!("Downloading history {history_url}")); - let https = hyper_tls::HttpsConnector::new(); - let response = hyper::Client::builder() - .build::<_, hyper::Body>(https) - .get(history_url.clone()) + let response = http::client() + .get(history_url.as_str()) + .send() .await .map_err(Error::DownloadingHistory)?; @@ -431,7 +433,8 @@ async fn get_history( return Err(Error::DownloadingHistoryGotStatusCode(response.status())); } - let body = hyper::body::to_bytes(response.into_body()) + let body = response + .bytes() .await .map_err(Error::ReadHistoryHttpStream)?; @@ -443,7 +446,7 @@ async fn get_history( async fn cache_bucket( print: &print::Print, - archive_url: &Uri, + archive_url: &Url, bucket_index: usize, bucket: &str, ) -> Result { @@ -458,11 +461,11 @@ async fn cache_bucket( print.globe(format!("Downloading bucket {bucket_index} {bucket}…")); - let bucket_url = Uri::from_str(&bucket_url).map_err(Error::ParsingBucketUrl)?; - let https = hyper_tls::HttpsConnector::new(); - let response = hyper::Client::builder() - .build::<_, hyper::Body>(https) - .get(bucket_url) + let bucket_url = Url::from_str(&bucket_url).map_err(Error::ParsingBucketUrl)?; + + let response = http::client() + .get(bucket_url.as_str()) + .send() .await .map_err(Error::GettingBucket)?; @@ -471,26 +474,22 @@ async fn cache_bucket( return Err(Error::GettingBucketGotStatusCode(response.status())); } - if let Some(val) = response.headers().get("Content-Length") { - if let Ok(str) = val.to_str() { - if let Ok(len) = str.parse::() { - print.clear_line(); - print.globe(format!( - "Downloaded bucket {bucket_index} {bucket} ({})", - ByteSize(len) - )); - } - } + if let Some(len) = response.content_length() { + print.clear_line(); + print.globe(format!( + "Downloaded bucket {bucket_index} {bucket} ({})", + ByteSize(len) + )); } print.println(""); - let read = response - .into_body() - .map(|result| result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))) - .into_async_read(); - let read = tokio_util::compat::FuturesAsyncReadCompatExt::compat(read); - let mut read = GzipDecoder::new(read); + let stream = response + .bytes_stream() + .map(|result| result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))); + let stream_reader = StreamReader::new(stream); + let buf_reader = BufReader::new(stream_reader); + let mut decoder = GzipDecoder::new(buf_reader); let dl_path = cache_path.with_extension("dl"); let mut file = OpenOptions::new() .create(true) @@ -499,7 +498,7 @@ async fn cache_bucket( .open(&dl_path) .await .map_err(Error::WriteOpeningCachedBucket)?; - tokio::io::copy(&mut read, &mut file) + tokio::io::copy(&mut decoder, &mut file) .await .map_err(Error::StreamingBucket)?; fs::rename(&dl_path, &cache_path).map_err(Error::RenameDownloadFile)?; diff --git a/cmd/soroban-cli/src/config/data.rs b/cmd/soroban-cli/src/config/data.rs index 23dedc619..bbfc6994e 100644 --- a/cmd/soroban-cli/src/config/data.rs +++ b/cmd/soroban-cli/src/config/data.rs @@ -1,8 +1,8 @@ use crate::rpc::{GetTransactionResponse, GetTransactionResponseRaw, SimulateTransactionResponse}; use directories::ProjectDirs; -use http::Uri; use serde::{Deserialize, Serialize}; use std::str::FromStr; +use url::Url; use crate::xdr::{self, WriteXdr}; @@ -15,7 +15,7 @@ pub enum Error { #[error(transparent)] SerdeJson(#[from] serde_json::Error), #[error(transparent)] - Http(#[from] http::uri::InvalidUri), + InvalidUrl(#[from] url::ParseError), #[error(transparent)] Ulid(#[from] ulid::DecodeError), #[error(transparent)] @@ -56,7 +56,7 @@ pub fn bucket_dir() -> Result { Ok(dir) } -pub fn write(action: Action, rpc_url: &Uri) -> Result { +pub fn write(action: Action, rpc_url: &Url) -> Result { let data = Data { action, rpc_url: rpc_url.to_string(), @@ -67,10 +67,10 @@ pub fn write(action: Action, rpc_url: &Uri) -> Result { Ok(id) } -pub fn read(id: &ulid::Ulid) -> Result<(Action, Uri), Error> { +pub fn read(id: &ulid::Ulid) -> Result<(Action, Url), Error> { let file = actions_dir()?.join(id.to_string()).with_extension("json"); let data: Data = serde_json::from_str(&std::fs::read_to_string(file)?)?; - Ok((data.action, http::Uri::from_str(&data.rpc_url)?)) + Ok((data.action, Url::from_str(&data.rpc_url)?)) } pub fn write_spec(hash: &str, spec_entries: &[xdr::ScSpecEntry]) -> Result<(), Error> { @@ -117,7 +117,7 @@ pub fn list_actions() -> Result, Error> { .collect::, Error>>() } -pub struct DatedAction(ulid::Ulid, Action, Uri); +pub struct DatedAction(ulid::Ulid, Action, Url); impl std::fmt::Display for DatedAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -200,7 +200,7 @@ mod test { fn test_write_read() { let t = assert_fs::TempDir::new().unwrap(); std::env::set_var(XDG_DATA_HOME, t.path().to_str().unwrap()); - let rpc_uri = http::uri::Uri::from_str("http://localhost:8000").unwrap(); + let rpc_uri = Url::from_str("http://localhost:8000").unwrap(); let sim = SimulateTransactionResponse::default(); let original_action: Action = sim.into(); diff --git a/cmd/soroban-cli/src/config/network.rs b/cmd/soroban-cli/src/config/network.rs index 9e1eabee3..b6f6d8c1d 100644 --- a/cmd/soroban-cli/src/config/network.rs +++ b/cmd/soroban-cli/src/config/network.rs @@ -1,32 +1,29 @@ -use std::str::FromStr; - use clap::arg; use phf::phf_map; use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::str::FromStr; use stellar_strkey::ed25519::PublicKey; +use url::Url; +use super::locator; +use crate::utils::http; use crate::{ commands::HEADING_RPC, rpc::{self, Client}, }; - -use super::locator; pub mod passphrase; #[derive(thiserror::Error, Debug)] pub enum Error { #[error(transparent)] Config(#[from] locator::Error), - #[error("network arg or rpc url and network passphrase are required if using the network")] Network, #[error(transparent)] - Http(#[from] http::Error), - #[error(transparent)] Rpc(#[from] rpc::Error), #[error(transparent)] - Hyper(#[from] hyper::Error), + HttpClient(#[from] reqwest::Error), #[error("Failed to parse JSON from {0}, {1}")] FailedToParseJSON(String, serde_json::Error), #[error("Invalid URL {0}")] @@ -107,29 +104,27 @@ pub struct Network { } impl Network { - pub async fn helper_url(&self, addr: &str) -> Result { - use http::Uri; + pub async fn helper_url(&self, addr: &str) -> Result { tracing::debug!("address {addr:?}"); - let rpc_uri = Uri::from_str(&self.rpc_url) + let rpc_url = Url::from_str(&self.rpc_url) .map_err(|_| Error::InvalidUrl(self.rpc_url.to_string()))?; if self.network_passphrase.as_str() == passphrase::LOCAL { - let auth = rpc_uri.authority().unwrap().clone(); - let scheme = rpc_uri.scheme_str().unwrap(); - Ok(Uri::builder() - .authority(auth) - .scheme(scheme) - .path_and_query(format!("/friendbot?addr={addr}")) - .build()?) + let mut local_url = rpc_url; + local_url.set_path("/friendbot"); + local_url.set_query(Some(&format!("addr={addr}"))); + Ok(local_url) } else { let client = Client::new(&self.rpc_url)?; let network = client.get_network().await?; tracing::debug!("network {network:?}"); - let uri = client.friendbot_url().await?; - tracing::debug!("URI {uri:?}"); - Uri::from_str(&format!("{uri}?addr={addr}")).map_err(|e| { + let url = client.friendbot_url().await?; + tracing::debug!("URL {url:?}"); + let mut url = Url::from_str(&url).map_err(|e| { tracing::error!("{e}"); - Error::InvalidUrl(uri.to_string()) - }) + Error::InvalidUrl(url.to_string()) + })?; + url.query_pairs_mut().append_pair("addr", addr); + Ok(url) } } @@ -137,21 +132,10 @@ impl Network { pub async fn fund_address(&self, addr: &PublicKey) -> Result<(), Error> { let uri = self.helper_url(&addr.to_string()).await?; tracing::debug!("URL {uri:?}"); - let response = match uri.scheme_str() { - Some("http") => hyper::Client::new().get(uri.clone()).await?, - Some("https") => { - let https = hyper_tls::HttpsConnector::new(); - hyper::Client::builder() - .build::<_, hyper::Body>(https) - .get(uri.clone()) - .await? - } - _ => { - return Err(Error::InvalidUrl(uri.to_string())); - } - }; + let response = http::client().get(uri.as_str()).send().await?; + let request_successful = response.status().is_success(); - let body = hyper::body::to_bytes(response.into_body()).await?; + let body = response.bytes().await?; let res = serde_json::from_slice::(&body) .map_err(|e| Error::FailedToParseJSON(uri.to_string(), e))?; tracing::debug!("{res:#?}"); @@ -173,8 +157,8 @@ impl Network { Ok(()) } - pub fn rpc_uri(&self) -> Result { - http::Uri::from_str(&self.rpc_url).map_err(|_| Error::InvalidUrl(self.rpc_url.to_string())) + pub fn rpc_uri(&self) -> Result { + Url::from_str(&self.rpc_url).map_err(|_| Error::InvalidUrl(self.rpc_url.to_string())) } } @@ -206,3 +190,90 @@ impl From<&(&str, &str)> for Network { } } } + +#[cfg(test)] +mod tests { + use super::*; + use mockito::Server; + use serde_json::json; + + #[tokio::test] + async fn test_helper_url_local_network() { + let network = Network { + rpc_url: "http://localhost:8000".to_string(), + network_passphrase: passphrase::LOCAL.to_string(), + }; + + let result = network + .helper_url("GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI") + .await; + + assert!(result.is_ok()); + let url = result.unwrap(); + assert_eq!(url.as_str(), "http://localhost:8000/friendbot?addr=GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"); + } + + #[tokio::test] + async fn test_helper_url_test_network() { + let mut server = Server::new_async().await; + let _mock = server + .mock("POST", "/") + .with_body_from_request(|req| { + let body: Value = serde_json::from_slice(req.body().unwrap()).unwrap(); + let id = body["id"].clone(); + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "friendbotUrl": "https://friendbot.stellar.org/", + "passphrase": passphrase::TESTNET.to_string(), + "protocolVersion": 21 + } + }) + .to_string() + .into() + }) + .create_async() + .await; + + let network = Network { + rpc_url: server.url(), + network_passphrase: passphrase::TESTNET.to_string(), + }; + let url = network + .helper_url("GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI") + .await + .unwrap(); + assert_eq!(url.as_str(), "https://friendbot.stellar.org/?addr=GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"); + } + + #[tokio::test] + async fn test_helper_url_test_network_with_path_and_params() { + let mut server = Server::new_async().await; + let _mock = server.mock("POST", "/") + .with_body_from_request(|req| { + let body: Value = serde_json::from_slice(req.body().unwrap()).unwrap(); + let id = body["id"].clone(); + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "friendbotUrl": "https://friendbot.stellar.org/secret?api_key=123456&user=demo", + "passphrase": passphrase::TESTNET.to_string(), + "protocolVersion": 21 + } + }).to_string().into() + }) + .create_async().await; + + let network = Network { + rpc_url: server.url(), + network_passphrase: passphrase::TESTNET.to_string(), + }; + let url = network + .helper_url("GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI") + .await + .unwrap(); + assert_eq!(url.as_str(), "https://friendbot.stellar.org/secret?api_key=123456&user=demo&addr=GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"); + } +} diff --git a/cmd/soroban-cli/src/upgrade_check.rs b/cmd/soroban-cli/src/upgrade_check.rs index ecd1a4adc..294056dd1 100644 --- a/cmd/soroban-cli/src/upgrade_check.rs +++ b/cmd/soroban-cli/src/upgrade_check.rs @@ -1,5 +1,6 @@ use crate::config::upgrade_check::UpgradeCheck; use crate::print::Print; +use crate::utils::http; use semver::Version; use serde::Deserialize; use std::error::Error; @@ -8,7 +9,6 @@ use std::time::Duration; const MINIMUM_CHECK_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); // 1 day const CRATES_IO_API_URL: &str = "https://crates.io/api/v1/crates/"; -const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); const NO_UPDATE_CHECK_ENV_VAR: &str = "STELLAR_NO_UPDATE_CHECK"; #[derive(Deserialize)] @@ -26,16 +26,20 @@ struct Crate { } /// Fetch the latest stable version of the crate from crates.io -fn fetch_latest_crate_info() -> Result> { +async fn fetch_latest_crate_info() -> Result> { let crate_name = env!("CARGO_PKG_NAME"); let url = format!("{CRATES_IO_API_URL}{crate_name}"); - let response = ureq::get(&url).timeout(REQUEST_TIMEOUT).call()?; - let crate_data: CrateResponse = response.into_json()?; - Ok(crate_data.crate_) + let resp = http::client() + .get(url) + .send() + .await? + .json::() + .await?; + Ok(resp.crate_) } /// Print a warning if a new version of the CLI is available -pub fn upgrade_check(quiet: bool) { +pub async fn upgrade_check(quiet: bool) { // We should skip the upgrade check if we're not in a tty environment. if !std::io::stderr().is_terminal() { return; @@ -59,7 +63,7 @@ pub fn upgrade_check(quiet: bool) { let now = chrono::Utc::now(); // Skip fetch from crates.io if we've checked recently if now - MINIMUM_CHECK_INTERVAL >= stats.latest_check_time { - match fetch_latest_crate_info() { + match fetch_latest_crate_info().await { Ok(c) => { stats = UpgradeCheck { latest_check_time: now, @@ -112,9 +116,9 @@ fn get_latest_version<'a>(current_version: &Version, stats: &'a UpgradeCheck) -> mod tests { use super::*; - #[test] - fn test_fetch_latest_stable_version() { - let _ = fetch_latest_crate_info().unwrap(); + #[tokio::test] + async fn test_fetch_latest_stable_version() { + let _ = fetch_latest_crate_info().await.unwrap(); } #[test] diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index 8d0090042..ee8bb1ece 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -161,6 +161,41 @@ pub fn get_name_from_stellar_asset_contract_storage(storage: &ScMap) -> Option String { + format!("{}/{}", env!("CARGO_PKG_NAME"), version::pkg()) + } + + /// Creates and returns a configured `reqwest::Client`. + /// + /// # Panics + /// + /// Panics if the Client initialization fails. + pub fn client() -> reqwest::Client { + // Why we panic here: + // 1. Client initialization failures are rare and usually indicate serious issues. + // 2. The application cannot function properly without a working HTTP client. + // 3. This simplifies error handling for callers, as they can assume a valid client. + reqwest::Client::builder() + .user_agent(user_agent()) + .build() + .expect("Failed to build reqwest client") + } + + /// Creates and returns a configured `reqwest::blocking::Client`. + /// + /// # Panics + /// + /// Panics if the Client initialization fails. + pub fn blocking_client() -> reqwest::blocking::Client { + reqwest::blocking::Client::builder() + .user_agent(user_agent()) + .build() + .expect("Failed to build reqwest blocking client") + } +} + pub mod rpc { use soroban_env_host::xdr; use soroban_rpc::{Client, Error}; From 88a8ca91a28afe0da63debb448b2d0a8060f99e5 Mon Sep 17 00:00:00 2001 From: Elizabeth Engelman <4752801+elizabethengelman@users.noreply.github.com> Date: Thu, 3 Oct 2024 10:33:13 -0400 Subject: [PATCH 6/8] Chore/use rich printer for network container (#1638) * Use rich printer in network container shared * Cleanup container retry logging --------- Co-authored-by: Nando Vieira --- .../src/commands/network/container.rs | 2 +- .../src/commands/network/container/logs.rs | 10 ++-- .../src/commands/network/container/shared.rs | 50 ++++++++++--------- .../src/commands/network/container/start.rs | 6 ++- .../src/commands/network/container/stop.rs | 2 +- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/cmd/soroban-cli/src/commands/network/container.rs b/cmd/soroban-cli/src/commands/network/container.rs index 463dbbdc8..d14dc72e4 100644 --- a/cmd/soroban-cli/src/commands/network/container.rs +++ b/cmd/soroban-cli/src/commands/network/container.rs @@ -41,7 +41,7 @@ pub enum Error { impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { match &self { - Cmd::Logs(cmd) => cmd.run().await?, + Cmd::Logs(cmd) => cmd.run(global_args).await?, Cmd::Start(cmd) => cmd.run(global_args).await?, Cmd::Stop(cmd) => cmd.run(global_args).await?, } diff --git a/cmd/soroban-cli/src/commands/network/container/logs.rs b/cmd/soroban-cli/src/commands/network/container/logs.rs index 99b36af9b..aaccffdde 100644 --- a/cmd/soroban-cli/src/commands/network/container/logs.rs +++ b/cmd/soroban-cli/src/commands/network/container/logs.rs @@ -1,6 +1,9 @@ use futures_util::TryStreamExt; -use crate::commands::network::container::shared::Error as ConnectionError; +use crate::{ + commands::{global, network::container::shared::Error as ConnectionError}, + print, +}; use super::shared::{Args, Name}; @@ -23,9 +26,10 @@ pub struct Cmd { } impl Cmd { - pub async fn run(&self) -> Result<(), Error> { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = print::Print::new(global_args.quiet); let container_name = Name(self.name.clone()).get_internal_container_name(); - let docker = self.container_args.connect_to_docker().await?; + let docker = self.container_args.connect_to_docker(&print).await?; let logs_stream = &mut docker.logs( &container_name, Some(bollard::container::LogsOptions { diff --git a/cmd/soroban-cli/src/commands/network/container/shared.rs b/cmd/soroban-cli/src/commands/network/container/shared.rs index f819f3ed3..38cb17af2 100644 --- a/cmd/soroban-cli/src/commands/network/container/shared.rs +++ b/cmd/soroban-cli/src/commands/network/container/shared.rs @@ -6,6 +6,8 @@ use clap::ValueEnum; // Need to add this for windows, since we are only using this crate for the unix fn try_docker_desktop_socket use home::home_dir; +use crate::print; + pub const DOCKER_HOST_HELP: &str = "Optional argument to override the default docker host. This is useful when you are using a non-standard docker host path for your Docker-compatible container runtime, e.g. Docker Desktop defaults to $HOME/.docker/run/docker.sock instead of /var/run/docker.sock"; // DEFAULT_DOCKER_HOST is from the bollard crate on the main branch, which has not been released yet: https://github.com/fussybeaver/bollard/blob/0972b1aac0ad5c08798e100319ddd0d2ee010365/src/docker.rs#L64 @@ -46,7 +48,8 @@ impl Args { .unwrap_or_default() } - pub(crate) async fn connect_to_docker(&self) -> Result { + #[allow(unused_variables)] + pub(crate) async fn connect_to_docker(&self, print: &print::Print) -> Result { // if no docker_host is provided, use the default docker host: // "unix:///var/run/docker.sock" on unix machines // "npipe:////./pipe/docker_engine" on windows machines @@ -89,7 +92,7 @@ impl Args { // if on unix, try to connect to the default docker desktop socket #[cfg(unix)] { - let docker_desktop_connection = try_docker_desktop_socket(&host)?; + let docker_desktop_connection = try_docker_desktop_socket(&host, print)?; match check_docker_connection(&docker_desktop_connection).await { Ok(()) => Ok(docker_desktop_connection), Err(err) => Err(err)?, @@ -138,40 +141,41 @@ impl Name { } #[cfg(unix)] -fn try_docker_desktop_socket(host: &str) -> Result { +fn try_docker_desktop_socket( + host: &str, + print: &print::Print, +) -> Result { let default_docker_desktop_host = format!("{}/.docker/run/docker.sock", home_dir().unwrap().display()); - println!("Failed to connect to DOCKER_HOST: {host}.\nTrying to connect to the default Docker Desktop socket at {default_docker_desktop_host}."); + print.warnln(format!("Failed to connect to Docker daemon at {host}.")); + + print.infoln(format!( + "Attempting to connect to the default Docker Desktop socket at {default_docker_desktop_host} instead." + )); Docker::connect_with_unix( &default_docker_desktop_host, DEFAULT_TIMEOUT, API_DEFAULT_VERSION, - ) + ).map_err(|e| { + print.errorln(format!( + "Failed to connect to the Docker daemon at {host:?}. Is the docker daemon running?" + )); + print.infoln( + "Running a local Stellar network requires a Docker-compatible container runtime." + ); + print.infoln( + "Please note that if you are using Docker Desktop, you may need to utilize the `--docker-host` flag to pass in the location of the docker socket on your machine." + ); + e + }) } // When bollard is not able to connect to the docker daemon, it returns a generic ConnectionRefused error // This method attempts to connect to the docker daemon and returns a more specific error message async fn check_docker_connection(docker: &Docker) -> Result<(), bollard::errors::Error> { - // This is a bit hacky, but the `client_addr` field is not directly accessible from the `Docker` struct, but we can access it from the debug string representation of the `Docker` struct - let docker_debug_string = format!("{docker:#?}"); - let start_of_client_addr = docker_debug_string.find("client_addr: ").unwrap(); - let end_of_client_addr = docker_debug_string[start_of_client_addr..] - .find(',') - .unwrap(); - // Extract the substring containing the value of client_addr - let client_addr = &docker_debug_string - [start_of_client_addr + "client_addr: ".len()..start_of_client_addr + end_of_client_addr] - .trim() - .trim_matches('"'); - match docker.version().await { Ok(_version) => Ok(()), - Err(err) => { - println!( - "⛔️ Failed to connect to the Docker daemon at {client_addr:?}. Is the docker daemon running?\nℹ️ Running a local Stellar network requires a Docker-compatible container runtime.\nℹ️ Please note that if you are using Docker Desktop, you may need to utilize the `--docker-host` flag to pass in the location of the docker socket on your machine.\n" - ); - Err(err) - } + Err(err) => Err(err), } } diff --git a/cmd/soroban-cli/src/commands/network/container/start.rs b/cmd/soroban-cli/src/commands/network/container/start.rs index a4d840f0f..2f16d3c1e 100644 --- a/cmd/soroban-cli/src/commands/network/container/start.rs +++ b/cmd/soroban-cli/src/commands/network/container/start.rs @@ -79,7 +79,11 @@ impl Runner { self.print .infoln(format!("Starting {} network", &self.args.network)); - let docker = self.args.container_args.connect_to_docker().await?; + let docker = self + .args + .container_args + .connect_to_docker(&self.print) + .await?; let image = self.get_image_name(); let mut stream = docker.create_image( diff --git a/cmd/soroban-cli/src/commands/network/container/stop.rs b/cmd/soroban-cli/src/commands/network/container/stop.rs index 87d6ba495..77f674c46 100644 --- a/cmd/soroban-cli/src/commands/network/container/stop.rs +++ b/cmd/soroban-cli/src/commands/network/container/stop.rs @@ -34,7 +34,7 @@ impl Cmd { pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { let print = print::Print::new(global_args.quiet); let container_name = Name(self.name.clone()); - let docker = self.container_args.connect_to_docker().await?; + let docker = self.container_args.connect_to_docker(&print).await?; print.infoln(format!( "Stopping {} container", From d37632aea26b68cfce22bb2d31e16727c56629d4 Mon Sep 17 00:00:00 2001 From: Abeeujah <100226788+Abeeujah@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:28:56 +0100 Subject: [PATCH 7/8] feat: Bring colors to help message (#1650) Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> --- cmd/soroban-cli/src/commands/global.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/cmd/soroban-cli/src/commands/global.rs b/cmd/soroban-cli/src/commands/global.rs index 9148e30cc..be883b6fd 100644 --- a/cmd/soroban-cli/src/commands/global.rs +++ b/cmd/soroban-cli/src/commands/global.rs @@ -1,11 +1,24 @@ -use clap::arg; +use clap::{ + arg, + builder::styling::{AnsiColor, Effects, Styles}, +}; use std::path::PathBuf; use super::config; +const USAGE_STYLES: Styles = Styles::styled() + .header(AnsiColor::Green.on_default().effects(Effects::BOLD)) + .usage(AnsiColor::Green.on_default().effects(Effects::BOLD)) + .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) + .placeholder(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) + .error(AnsiColor::Red.on_default().effects(Effects::BOLD)) + .valid(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) + .invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD)); + #[derive(Debug, clap::Args, Clone, Default)] #[group(skip)] #[allow(clippy::struct_excessive_bools)] +#[command(styles = USAGE_STYLES)] pub struct Args { #[clap(flatten)] pub locator: config::locator::Args, From b940978a5a61fa7ddfcb75235cc2e5796f26c22e Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Sat, 5 Oct 2024 01:29:02 +1000 Subject: [PATCH 8/8] Add Assembled from stellar-rpc-client@9f74833 (#1648) --- .../soroban-test/tests/it/integration/tx.rs | 5 +- cmd/soroban-cli/src/assembled.rs | 541 ++++++++++++++++++ .../src/commands/contract/deploy/asset.rs | 3 +- .../src/commands/contract/deploy/wasm.rs | 3 +- .../src/commands/contract/extend.rs | 4 +- .../src/commands/contract/install.rs | 5 +- .../src/commands/contract/invoke.rs | 3 +- cmd/soroban-cli/src/commands/tx/simulate.rs | 9 +- cmd/soroban-cli/src/fee.rs | 2 +- cmd/soroban-cli/src/lib.rs | 1 + 10 files changed, 561 insertions(+), 15 deletions(-) create mode 100644 cmd/soroban-cli/src/assembled.rs diff --git a/cmd/crates/soroban-test/tests/it/integration/tx.rs b/cmd/crates/soroban-test/tests/it/integration/tx.rs index 66c4b69fe..5cc6bdb9b 100644 --- a/cmd/crates/soroban-test/tests/it/integration/tx.rs +++ b/cmd/crates/soroban-test/tests/it/integration/tx.rs @@ -1,3 +1,4 @@ +use soroban_cli::assembled::simulate_and_assemble_transaction; use soroban_sdk::xdr::{Limits, ReadXdr, TransactionEnvelope, WriteXdr}; use soroban_test::{AssertExt, TestEnv}; @@ -23,9 +24,7 @@ async fn simulate() { .success() .stdout_as_str(); assert_eq!(xdr_base64_sim_only, assembled_str); - let assembled = sandbox - .client() - .simulate_and_assemble_transaction(&tx) + let assembled = simulate_and_assemble_transaction(&sandbox.client(), &tx) .await .unwrap(); let txn_env: TransactionEnvelope = assembled.transaction().clone().into(); diff --git a/cmd/soroban-cli/src/assembled.rs b/cmd/soroban-cli/src/assembled.rs new file mode 100644 index 000000000..312863d52 --- /dev/null +++ b/cmd/soroban-cli/src/assembled.rs @@ -0,0 +1,541 @@ +use sha2::{Digest, Sha256}; +use stellar_xdr::curr::{ + self as xdr, ExtensionPoint, Hash, InvokeHostFunctionOp, LedgerFootprint, Limits, Memo, + Operation, OperationBody, Preconditions, ReadXdr, RestoreFootprintOp, + SorobanAuthorizationEntry, SorobanAuthorizedFunction, SorobanResources, SorobanTransactionData, + Transaction, TransactionEnvelope, TransactionExt, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, VecM, WriteXdr, +}; + +use soroban_rpc::{Error, RestorePreamble, SimulateTransactionResponse}; + +use soroban_rpc::{LogEvents, LogResources}; + +pub(crate) const DEFAULT_TRANSACTION_FEES: u32 = 100; + +pub async fn simulate_and_assemble_transaction( + client: &soroban_rpc::Client, + tx: &Transaction, +) -> Result { + let sim_res = client + .simulate_transaction_envelope(&TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: VecM::default(), + })) + .await?; + match sim_res.error { + None => Ok(Assembled::new(tx, sim_res)?), + Some(e) => { + diagnostic_events(&sim_res.events, tracing::Level::ERROR); + Err(Error::TransactionSimulationFailed(e)) + } + } +} + +pub struct Assembled { + pub(crate) txn: Transaction, + pub(crate) sim_res: SimulateTransactionResponse, +} + +/// Represents an assembled transaction ready to be signed and submitted to the network. +impl Assembled { + /// + /// Creates a new `Assembled` transaction. + /// + /// # Arguments + /// + /// * `txn` - The original transaction. + /// * `client` - The client used for simulation and submission. + /// + /// # Errors + /// + /// Returns an error if simulation fails or if assembling the transaction fails. + pub fn new(txn: &Transaction, sim_res: SimulateTransactionResponse) -> Result { + let txn = assemble(txn, &sim_res)?; + Ok(Self { txn, sim_res }) + } + + /// + /// Calculates the hash of the assembled transaction. + /// + /// # Arguments + /// + /// * `network_passphrase` - The network passphrase. + /// + /// # Errors + /// + /// Returns an error if generating the hash fails. + pub fn hash(&self, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { + let signature_payload = TransactionSignaturePayload { + network_id: Hash(Sha256::digest(network_passphrase).into()), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(self.txn.clone()), + }; + Ok(Sha256::digest(signature_payload.to_xdr(Limits::none())?).into()) + } + + /// Create a transaction for restoring any data in the `restore_preamble` field of the `SimulateTransactionResponse`. + /// + /// # Errors + pub fn restore_txn(&self) -> Result, Error> { + if let Some(restore_preamble) = &self.sim_res.restore_preamble { + restore(self.transaction(), restore_preamble).map(Option::Some) + } else { + Ok(None) + } + } + + /// Returns a reference to the original transaction. + #[must_use] + pub fn transaction(&self) -> &Transaction { + &self.txn + } + + /// Returns a reference to the simulation response. + #[must_use] + pub fn sim_response(&self) -> &SimulateTransactionResponse { + &self.sim_res + } + + #[must_use] + pub fn bump_seq_num(mut self) -> Self { + self.txn.seq_num.0 += 1; + self + } + + /// + /// # Errors + #[must_use] + pub fn auth_entries(&self) -> VecM { + self.txn + .operations + .first() + .and_then(|op| match op.body { + OperationBody::InvokeHostFunction(ref body) => (matches!( + body.auth.first().map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + )) + .then_some(body.auth.clone()), + _ => None, + }) + .unwrap_or_default() + } + + /// + /// # Errors + pub fn log( + &self, + log_events: Option, + log_resources: Option, + ) -> Result<(), Error> { + if let TransactionExt::V1(SorobanTransactionData { + resources: resources @ SorobanResources { footprint, .. }, + .. + }) = &self.txn.ext + { + if let Some(log) = log_resources { + log(resources); + } + if let Some(log) = log_events { + log(footprint, &[self.auth_entries()], &self.sim_res.events()?); + }; + } + Ok(()) + } + + #[must_use] + pub fn requires_auth(&self) -> bool { + requires_auth(&self.txn).is_some() + } + + #[must_use] + pub fn is_view(&self) -> bool { + let TransactionExt::V1(SorobanTransactionData { + resources: + SorobanResources { + footprint: LedgerFootprint { read_write, .. }, + .. + }, + .. + }) = &self.txn.ext + else { + return false; + }; + read_write.is_empty() + } + + #[must_use] + pub fn set_max_instructions(mut self, instructions: u32) -> Self { + if let TransactionExt::V1(SorobanTransactionData { + resources: + SorobanResources { + instructions: ref mut i, + .. + }, + .. + }) = &mut self.txn.ext + { + tracing::trace!("setting max instructions to {instructions} from {i}"); + *i = instructions; + } + self + } +} + +// Apply the result of a simulateTransaction onto a transaction envelope, preparing it for +// submission to the network. +/// +/// # Errors +fn assemble( + raw: &Transaction, + simulation: &SimulateTransactionResponse, +) -> Result { + let mut tx = raw.clone(); + + // Right now simulate.results is one-result-per-function, and assumes there is only one + // operation in the txn, so we need to enforce that here. I (Paul) think that is a bug + // in soroban-rpc.simulateTransaction design, and we should fix it there. + // TODO: We should to better handling so non-soroban txns can be a passthrough here. + if tx.operations.len() != 1 { + return Err(Error::UnexpectedOperationCount { + count: tx.operations.len(), + }); + } + + let transaction_data = simulation.transaction_data()?; + + let mut op = tx.operations[0].clone(); + if let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body { + if body.auth.is_empty() { + if simulation.results.len() != 1 { + return Err(Error::UnexpectedSimulateTransactionResultSize { + length: simulation.results.len(), + }); + } + + let auths = simulation + .results + .iter() + .map(|r| { + VecM::try_from( + r.auth + .iter() + .map(|v| SorobanAuthorizationEntry::from_xdr_base64(v, Limits::none())) + .collect::, _>>()?, + ) + }) + .collect::, _>>()?; + if !auths.is_empty() { + body.auth = auths[0].clone(); + } + } + } + + // Update transaction fees to meet the minimum resource fees. + let classic_tx_fee: u64 = DEFAULT_TRANSACTION_FEES.into(); + + // Choose larger of existing fee or inclusion + resource fee. + tx.fee = tx.fee.max( + u32::try_from(classic_tx_fee + simulation.min_resource_fee) + .map_err(|_| Error::LargeFee(simulation.min_resource_fee + classic_tx_fee))?, + ); + + tx.operations = vec![op].try_into()?; + tx.ext = TransactionExt::V1(transaction_data); + Ok(tx) +} + +fn requires_auth(txn: &Transaction) -> Option { + let [op @ Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] = txn.operations.as_slice() + else { + return None; + }; + matches!( + auth.first().map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) + .then(move || op.clone()) +} + +fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { + let transaction_data = + SorobanTransactionData::from_xdr_base64(&restore.transaction_data, Limits::none())?; + let fee = u32::try_from(restore.min_resource_fee) + .map_err(|_| Error::LargeFee(restore.min_resource_fee))?; + Ok(Transaction { + source_account: parent.source_account.clone(), + fee: parent + .fee + .checked_add(fee) + .ok_or(Error::LargeFee(restore.min_resource_fee))?, + seq_num: parent.seq_num.clone(), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![Operation { + source_account: None, + body: OperationBody::RestoreFootprint(RestoreFootprintOp { + ext: ExtensionPoint::V0, + }), + }] + .try_into()?, + ext: TransactionExt::V1(transaction_data), + }) +} + +fn diagnostic_events(events: &[impl std::fmt::Debug], level: tracing::Level) { + for (i, event) in events.iter().enumerate() { + if level == tracing::Level::TRACE { + tracing::trace!("{i}: {event:#?}"); + } else if level == tracing::Level::INFO { + tracing::info!("{i}: {event:#?}"); + } else if level == tracing::Level::ERROR { + tracing::error!("{i}: {event:#?}"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use soroban_rpc::SimulateHostFunctionResultRaw; + use stellar_strkey::ed25519::PublicKey as Ed25519PublicKey; + use stellar_xdr::curr::{ + AccountId, ChangeTrustAsset, ChangeTrustOp, ExtensionPoint, Hash, HostFunction, + InvokeContractArgs, InvokeHostFunctionOp, LedgerFootprint, Memo, MuxedAccount, Operation, + Preconditions, PublicKey, ScAddress, ScSymbol, ScVal, SequenceNumber, + SorobanAuthorizedFunction, SorobanAuthorizedInvocation, SorobanResources, + SorobanTransactionData, Uint256, WriteXdr, + }; + + const SOURCE: &str = "GBZXN7PIRZGNMHGA7MUUUF4GWPY5AYPV6LY4UV2GL6VJGIQRXFDNMADI"; + + fn transaction_data() -> SorobanTransactionData { + SorobanTransactionData { + resources: SorobanResources { + footprint: LedgerFootprint { + read_only: VecM::default(), + read_write: VecM::default(), + }, + instructions: 0, + read_bytes: 5, + write_bytes: 0, + }, + resource_fee: 0, + ext: ExtensionPoint::V0, + } + } + + fn simulation_response() -> SimulateTransactionResponse { + let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; + let fn_auth = &SorobanAuthorizationEntry { + credentials: xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials { + address: ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256( + source_bytes, + )))), + nonce: 0, + signature_expiration_ledger: 0, + signature: ScVal::Void, + }), + root_invocation: SorobanAuthorizedInvocation { + function: SorobanAuthorizedFunction::ContractFn(InvokeContractArgs { + contract_address: ScAddress::Contract(Hash([0; 32])), + function_name: ScSymbol("fn".try_into().unwrap()), + args: VecM::default(), + }), + sub_invocations: VecM::default(), + }, + }; + + SimulateTransactionResponse { + min_resource_fee: 115, + latest_ledger: 3, + results: vec![SimulateHostFunctionResultRaw { + auth: vec![fn_auth.to_xdr_base64(Limits::none()).unwrap()], + xdr: ScVal::U32(0).to_xdr_base64(Limits::none()).unwrap(), + }], + transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), + ..Default::default() + } + } + + fn single_contract_fn_transaction() -> Transaction { + let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; + Transaction { + source_account: MuxedAccount::Ed25519(Uint256(source_bytes)), + fee: 100, + seq_num: SequenceNumber(0), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![Operation { + source_account: None, + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { + host_function: HostFunction::InvokeContract(InvokeContractArgs { + contract_address: ScAddress::Contract(Hash([0x0; 32])), + function_name: ScSymbol::default(), + args: VecM::default(), + }), + auth: VecM::default(), + }), + }] + .try_into() + .unwrap(), + ext: TransactionExt::V0, + } + } + + #[test] + fn test_assemble_transaction_updates_tx_data_from_simulation_response() { + let sim = simulation_response(); + let txn = single_contract_fn_transaction(); + let Ok(result) = assemble(&txn, &sim) else { + panic!("assemble failed"); + }; + + // validate it auto updated the tx fees from sim response fees + // since it was greater than tx.fee + assert_eq!(215, result.fee); + + // validate it updated sorobantransactiondata block in the tx ext + assert_eq!(TransactionExt::V1(transaction_data()), result.ext); + } + + #[test] + fn test_assemble_transaction_adds_the_auth_to_the_host_function() { + let sim = simulation_response(); + let txn = single_contract_fn_transaction(); + let Ok(result) = assemble(&txn, &sim) else { + panic!("assemble failed"); + }; + + assert_eq!(1, result.operations.len()); + let OperationBody::InvokeHostFunction(ref op) = result.operations[0].body else { + panic!("unexpected operation type: {:#?}", result.operations[0]); + }; + + assert_eq!(1, op.auth.len()); + let auth = &op.auth[0]; + + let xdr::SorobanAuthorizedFunction::ContractFn(xdr::InvokeContractArgs { + ref function_name, + .. + }) = auth.root_invocation.function + else { + panic!("unexpected function type"); + }; + assert_eq!("fn".to_string(), format!("{}", function_name.0)); + + let xdr::SorobanCredentials::Address(xdr::SorobanAddressCredentials { + address: + xdr::ScAddress::Account(xdr::AccountId(xdr::PublicKey::PublicKeyTypeEd25519(address))), + .. + }) = &auth.credentials + else { + panic!("unexpected credentials type"); + }; + assert_eq!( + SOURCE.to_string(), + stellar_strkey::ed25519::PublicKey(address.0).to_string() + ); + } + + #[test] + fn test_assemble_transaction_errors_for_non_invokehostfn_ops() { + let source_bytes = Ed25519PublicKey::from_string(SOURCE).unwrap().0; + let txn = Transaction { + source_account: MuxedAccount::Ed25519(Uint256(source_bytes)), + fee: 100, + seq_num: SequenceNumber(0), + cond: Preconditions::None, + memo: Memo::None, + operations: vec![Operation { + source_account: None, + body: OperationBody::ChangeTrust(ChangeTrustOp { + line: ChangeTrustAsset::Native, + limit: 0, + }), + }] + .try_into() + .unwrap(), + ext: TransactionExt::V0, + }; + + let result = assemble( + &txn, + &SimulateTransactionResponse { + min_resource_fee: 115, + transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), + latest_ledger: 3, + ..Default::default() + }, + ); + + match result { + Ok(_) => {} + Err(e) => panic!("expected assembled operation, got: {e:#?}"), + } + } + + #[test] + fn test_assemble_transaction_errors_for_errors_for_mismatched_simulation() { + let txn = single_contract_fn_transaction(); + + let result = assemble( + &txn, + &SimulateTransactionResponse { + min_resource_fee: 115, + transaction_data: transaction_data().to_xdr_base64(Limits::none()).unwrap(), + latest_ledger: 3, + ..Default::default() + }, + ); + + match result { + Err(Error::UnexpectedSimulateTransactionResultSize { length }) => { + assert_eq!(0, length); + } + r => panic!("expected UnexpectedSimulateTransactionResultSize error, got: {r:#?}"), + } + } + + #[test] + fn test_assemble_transaction_overflow_behavior() { + // + // Test two separate cases: + // + // 1. Given a near-max (u32::MAX - 100) resource fee make sure the tx + // fee does not overflow after adding the base inclusion fee (100). + // 2. Given a large resource fee that WILL exceed u32::MAX with the + // base inclusion fee, ensure the overflow is caught with an error + // rather than silently ignored. + let txn = single_contract_fn_transaction(); + let mut response = simulation_response(); + + // sanity check so these can be adjusted if the above helper changes + assert_eq!(txn.fee, 100, "modified txn.fee: update the math below"); + + // 1: wiggle room math overflows but result fits + response.min_resource_fee = (u32::MAX - 100).into(); + + match assemble(&txn, &response) { + Ok(asstxn) => { + let expected = u32::MAX; + assert_eq!(asstxn.fee, expected); + } + r => panic!("expected success, got: {r:#?}"), + } + + // 2: combo overflows, should throw + response.min_resource_fee = (u32::MAX - 99).into(); + + match assemble(&txn, &response) { + Err(Error::LargeFee(fee)) => { + let expected = u64::from(u32::MAX) + 1; + assert_eq!(expected, fee, "expected {expected} != {fee} actual"); + } + r => panic!("expected LargeFee error, got: {r:#?}"), + } + } +} diff --git a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs index d2fcb62c4..8ff8e08b3 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/asset.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/asset.rs @@ -12,6 +12,7 @@ use std::convert::Infallible; use std::{array::TryFromSliceError, fmt::Debug, num::ParseIntError}; use crate::{ + assembled::simulate_and_assemble_transaction, commands::{ global, txn_result::{TxnEnvelopeResult, TxnResult}, @@ -118,7 +119,7 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let txn = client.simulate_and_assemble_transaction(&tx).await?; + let txn = simulate_and_assemble_transaction(&client, &tx).await?; let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { return Ok(TxnResult::Txn(txn)); diff --git a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs index 42b31dd7d..36ce2e9f7 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy/wasm.rs @@ -16,6 +16,7 @@ use soroban_env_host::{ }; use crate::{ + assembled::simulate_and_assemble_transaction, commands::{contract::install, HEADING_RPC}, config::{self, data, locator, network}, rpc::{self, Client}, @@ -240,7 +241,7 @@ impl NetworkRunnable for Cmd { print.infoln("Simulating deploy transaction…"); - let txn = client.simulate_and_assemble_transaction(&txn).await?; + let txn = simulate_and_assemble_transaction(&client, &txn).await?; let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { diff --git a/cmd/soroban-cli/src/commands/contract/extend.rs b/cmd/soroban-cli/src/commands/contract/extend.rs index b06cacf3e..3f7477254 100644 --- a/cmd/soroban-cli/src/commands/contract/extend.rs +++ b/cmd/soroban-cli/src/commands/contract/extend.rs @@ -9,6 +9,7 @@ use soroban_env_host::xdr::{ }; use crate::{ + assembled::simulate_and_assemble_transaction, commands::{ global, txn_result::{TxnEnvelopeResult, TxnResult}, @@ -170,8 +171,7 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let tx = client - .simulate_and_assemble_transaction(&tx) + let tx = simulate_and_assemble_transaction(&client, &tx) .await? .transaction() .clone(); diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 9e8f6b098..b70cadb78 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -10,6 +10,7 @@ use soroban_env_host::xdr::{ }; use super::restore; +use crate::assembled::simulate_and_assemble_transaction; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::{global, NetworkRunnable}; use crate::config::{self, data, network}; @@ -177,9 +178,7 @@ impl NetworkRunnable for Cmd { print.infoln("Simulating install transaction…"); - let txn = client - .simulate_and_assemble_transaction(&tx_without_preflight) - .await?; + let txn = simulate_and_assemble_transaction(&client, &tx_without_preflight).await?; let txn = self.fee.apply_to_assembled_txn(txn).transaction().clone(); if self.fee.sim_only { diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index fa90ef9b3..55e2554bf 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -23,6 +23,7 @@ use soroban_spec::read::FromWasmError; use super::super::events; use super::arg_parsing; +use crate::assembled::simulate_and_assemble_transaction; use crate::commands::contract::arg_parsing::{build_host_function_parameters, output_to_string}; use crate::commands::txn_result::{TxnEnvelopeResult, TxnResult}; use crate::commands::NetworkRunnable; @@ -246,7 +247,7 @@ impl NetworkRunnable for Cmd { if self.fee.build_only { return Ok(TxnResult::Txn(tx)); } - let txn = client.simulate_and_assemble_transaction(&tx).await?; + let txn = simulate_and_assemble_transaction(&client, &tx).await?; let txn = self.fee.apply_to_assembled_txn(txn); if self.fee.sim_only { return Ok(TxnResult::Txn(txn.transaction().clone())); diff --git a/cmd/soroban-cli/src/commands/tx/simulate.rs b/cmd/soroban-cli/src/commands/tx/simulate.rs index 58be37deb..1f534884d 100644 --- a/cmd/soroban-cli/src/commands/tx/simulate.rs +++ b/cmd/soroban-cli/src/commands/tx/simulate.rs @@ -1,6 +1,8 @@ -use crate::xdr::{self, TransactionEnvelope, WriteXdr}; +use crate::{ + assembled::{simulate_and_assemble_transaction, Assembled}, + xdr::{self, TransactionEnvelope, WriteXdr}, +}; use async_trait::async_trait; -use soroban_rpc::Assembled; use crate::commands::{config, global, NetworkRunnable}; @@ -50,6 +52,7 @@ impl NetworkRunnable for Cmd { let network = config.get_network()?; let client = crate::rpc::Client::new(&network.rpc_url)?; let tx = super::xdr::unwrap_envelope_v1(super::xdr::tx_envelope_from_stdin()?)?; - Ok(client.simulate_and_assemble_transaction(&tx).await?) + let tx = simulate_and_assemble_transaction(&client, &tx).await?; + Ok(tx) } } diff --git a/cmd/soroban-cli/src/fee.rs b/cmd/soroban-cli/src/fee.rs index 698d66007..c2bc32bdb 100644 --- a/cmd/soroban-cli/src/fee.rs +++ b/cmd/soroban-cli/src/fee.rs @@ -1,7 +1,7 @@ use clap::arg; +use crate::assembled::Assembled; use soroban_env_host::xdr; -use soroban_rpc::Assembled; use crate::commands::HEADING_RPC; diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index f5ea21884..4c904855c 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -11,6 +11,7 @@ pub(crate) use soroban_rpc as rpc; mod cli; pub use cli::main; +pub mod assembled; pub mod commands; pub mod config; pub mod fee;